Skip to main content

rig_core/model/
listing.rs

1//! Model listing types and error handling.
2//!
3//! This module provides types for representing available models from providers.
4//! All models are returned in a single list; providers with pagination
5//! handle fetching all pages internally.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Represents a single model available from a provider.
11///
12/// This struct is designed to be flexible enough to accommodate the varying
13/// responses from different LLM providers while providing a common interface.
14///
15/// # Fields
16///
17/// - `id`: The unique identifier for the model (required)
18/// - `name`: A human-readable name for the model
19/// - `description`: A detailed description of the model's capabilities
20/// - `r#type`: The type of model (e.g., "chat", "completion", "embedding")
21/// - `created_at`: Timestamp when the model was created
22/// - `owned_by`: The organization or entity that owns the model
23/// - `context_length`: The maximum context window size for the model
24/// - `max_output_tokens`: The maximum tokens the model may generate per response
25///
26/// # Example
27///
28/// ```rust
29/// use rig_core::model::Model;
30///
31/// // Create a model with just an ID
32/// let model = Model::from_id("gpt-4");
33///
34/// // Create a model with ID and name
35/// let model = Model::new("gpt-4", "GPT-4");
36///
37/// // Create a model with all fields
38/// let model = Model {
39///     id: "gpt-4".to_string(),
40///     name: Some("GPT-4".to_string()),
41///     description: Some("A large language model...".to_string()),
42///     r#type: Some("chat".to_string()),
43///     created_at: Some(1677610600),
44///     owned_by: Some("openai".to_string()),
45///     context_length: Some(8192),
46///     max_output_tokens: Some(4096),
47/// };
48/// ```
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct Model {
51    /// The unique identifier for the model (required)
52    pub id: String,
53
54    /// A human-readable name for the model
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub name: Option<String>,
57
58    /// A detailed description of the model's capabilities
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub description: Option<String>,
61
62    /// The type of model (e.g., "chat", "completion", "embedding")
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[serde(rename = "type")]
65    pub r#type: Option<String>,
66
67    /// Timestamp when the model was created (Unix epoch)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub created_at: Option<u64>,
70
71    /// The organization or entity that owns the model
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub owned_by: Option<String>,
74
75    /// The maximum context window size for the model
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub context_length: Option<u32>,
78
79    /// The maximum number of tokens the model may generate in one response.
80    ///
81    /// Distinct from [`Self::context_length`]: that is the input window, this
82    /// is the output ceiling, and for most models the output ceiling is far
83    /// smaller (Gemini 2.5 Flash: 1,048,576 in, 65,536 out).
84    ///
85    /// `None` means the provider's listing does not report one — never a
86    /// default rig invented. Rig does **not** send this value on requests:
87    /// omitting an output limit lets the provider apply its own per-model
88    /// default, and populating it from here would reintroduce a rig-chosen cap
89    /// by another route (rig#2322). It is for callers and diagnostics.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub max_output_tokens: Option<u32>,
92}
93
94impl Model {
95    /// Creates a new Model with the given ID and name.
96    ///
97    /// # Arguments
98    ///
99    /// * `id` - The unique identifier for the model
100    /// * `name` - A human-readable name for the model
101    ///
102    /// # Example
103    ///
104    /// ```rust
105    /// use rig_core::model::Model;
106    ///
107    /// let model = Model::new("gpt-4", "GPT-4");
108    /// assert_eq!(model.id, "gpt-4");
109    /// assert_eq!(model.name, Some("GPT-4".to_string()));
110    /// ```
111    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
112        Self {
113            id: id.into(),
114            name: Some(name.into()),
115            description: None,
116            r#type: None,
117            created_at: None,
118            owned_by: None,
119            context_length: None,
120            max_output_tokens: None,
121        }
122    }
123
124    /// Creates a new Model with only the required ID field.
125    ///
126    /// # Arguments
127    ///
128    /// * `id` - The unique identifier for the model
129    ///
130    /// # Example
131    ///
132    /// ```rust
133    /// use rig_core::model::Model;
134    ///
135    /// let model = Model::from_id("gpt-4");
136    /// assert_eq!(model.id, "gpt-4");
137    /// assert_eq!(model.name, None);
138    /// ```
139    pub fn from_id(id: impl Into<String>) -> Self {
140        Self {
141            id: id.into(),
142            name: None,
143            description: None,
144            r#type: None,
145            created_at: None,
146            owned_by: None,
147            context_length: None,
148            max_output_tokens: None,
149        }
150    }
151
152    /// Returns a reference to the model's name, or the ID if no name is set.
153    ///
154    /// This is useful for display purposes when you want to show the most
155    /// human-readable identifier available.
156    ///
157    /// # Example
158    ///
159    /// ```rust
160    /// use rig_core::model::Model;
161    ///
162    /// let model_with_name = Model::new("gpt-4", "GPT-4");
163    /// assert_eq!(model_with_name.display_name(), "GPT-4");
164    ///
165    /// let model_without_name = Model::from_id("gpt-4");
166    /// assert_eq!(model_without_name.display_name(), "gpt-4");
167    /// ```
168    pub fn display_name(&self) -> &str {
169        self.name.as_ref().unwrap_or(&self.id)
170    }
171}
172
173impl fmt::Display for Model {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        write!(f, "{}", self.display_name())
176    }
177}
178
179/// Represents a complete list of models from a provider.
180///
181/// This struct contains all available models from a provider. Providers that
182/// support pagination internally handle fetching all pages before returning results.
183///
184/// # Fields
185///
186/// - `data`: The complete list of available models
187///
188/// # Example
189///
190/// ```rust
191/// use rig_core::model::{Model, ModelList};
192///
193/// let list = ModelList::new(vec![
194///     Model::from_id("gpt-4"),
195///     Model::from_id("gpt-3.5-turbo"),
196/// ]);
197///
198/// println!("Found {} models", list.len());
199/// for model in list.iter() {
200///     println!("- {}", model.display_name());
201/// }
202/// ```
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ModelList {
205    /// The complete list of available models
206    pub data: Vec<Model>,
207}
208
209impl ModelList {
210    /// Creates a new ModelList with the given models.
211    ///
212    /// # Arguments
213    ///
214    /// * `data` - The list of models
215    ///
216    /// # Example
217    ///
218    /// ```rust
219    /// use rig_core::model::{Model, ModelList};
220    ///
221    /// let list = ModelList::new(vec![
222    ///     Model::from_id("gpt-4"),
223    ///     Model::from_id("gpt-3.5-turbo"),
224    /// ]);
225    /// assert_eq!(list.len(), 2);
226    /// ```
227    pub fn new(data: Vec<Model>) -> Self {
228        Self { data }
229    }
230
231    /// Returns true if the list is empty.
232    ///
233    /// # Example
234    ///
235    /// ```rust
236    /// use rig_core::model::ModelList;
237    ///
238    /// let empty = ModelList::new(vec![]);
239    /// assert!(empty.is_empty());
240    ///
241    /// let non_empty = ModelList::new(vec![rig_core::model::Model::from_id("gpt-4")]);
242    /// assert!(!non_empty.is_empty());
243    /// ```
244    pub fn is_empty(&self) -> bool {
245        self.data.is_empty()
246    }
247
248    /// Returns the number of models in this list.
249    ///
250    /// # Example
251    ///
252    /// ```rust
253    /// use rig_core::model::{Model, ModelList};
254    ///
255    /// let list = ModelList::new(vec![
256    ///     Model::from_id("gpt-4"),
257    ///     Model::from_id("gpt-3.5-turbo"),
258    /// ]);
259    /// assert_eq!(list.len(), 2);
260    /// ```
261    pub fn len(&self) -> usize {
262        self.data.len()
263    }
264
265    /// Returns an iterator over the models in this list.
266    ///
267    /// # Example
268    ///
269    /// ```rust
270    /// use rig_core::model::{Model, ModelList};
271    ///
272    /// let list = ModelList::new(vec![
273    ///     Model::from_id("gpt-4"),
274    ///     Model::from_id("gpt-3.5-turbo"),
275    /// ]);
276    ///
277    /// for model in list.iter() {
278    ///     println!("Model: {}", model.display_name());
279    /// }
280    /// ```
281    pub fn iter(&self) -> std::slice::Iter<'_, Model> {
282        self.data.iter()
283    }
284}
285
286impl IntoIterator for ModelList {
287    type Item = Model;
288    type IntoIter = std::vec::IntoIter<Model>;
289
290    fn into_iter(self) -> Self::IntoIter {
291        self.data.into_iter()
292    }
293}
294
295impl<'a> IntoIterator for &'a ModelList {
296    type Item = &'a Model;
297    type IntoIter = std::slice::Iter<'a, Model>;
298
299    fn into_iter(self) -> Self::IntoIter {
300        self.data.iter()
301    }
302}
303
304/// Errors that can occur when listing models from a provider.
305///
306/// This enum represents the various error conditions that may arise when
307/// attempting to retrieve the list of available models from an LLM provider.
308#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
309pub enum ModelListingError {
310    /// The provider returned an error response with a status code
311    #[error("API error (status {status_code}): {message}")]
312    ApiError {
313        /// HTTP status code
314        status_code: u16,
315        /// Error message from the provider
316        message: String,
317    },
318
319    /// Failed to send the request to the provider
320    #[error("Request error: {message}")]
321    RequestError {
322        /// Description of the request error
323        message: String,
324    },
325
326    /// Failed to parse the provider's response
327    #[error("Parse error: {message}")]
328    ParseError {
329        /// Description of the parsing error
330        message: String,
331    },
332
333    /// Authentication failed (invalid API key, etc.)
334    #[error("Authentication error: {message}")]
335    AuthError {
336        /// Authentication error details
337        message: String,
338    },
339}
340
341const RESPONSE_BODY_PREVIEW_LIMIT: usize = 2048;
342
343fn format_response_body_preview(body: &[u8]) -> String {
344    let preview_len = body.len().min(RESPONSE_BODY_PREVIEW_LIMIT);
345    let preview_bytes = body.get(..preview_len).unwrap_or(body);
346    let mut preview = String::from_utf8_lossy(preview_bytes).into_owned();
347
348    if body.len() > RESPONSE_BODY_PREVIEW_LIMIT {
349        preview.push_str(&format!(
350            "\n...<truncated {} bytes>",
351            body.len() - RESPONSE_BODY_PREVIEW_LIMIT
352        ));
353    }
354
355    preview
356}
357
358fn format_response_context(
359    provider: &str,
360    path: &str,
361    details: impl fmt::Display,
362    body: &[u8],
363) -> String {
364    format!(
365        "provider={provider}\npath={path}\n{details}\nbody_bytes={}\nresponse_body_preview:\n{}",
366        body.len(),
367        format_response_body_preview(body)
368    )
369}
370
371impl ModelListingError {
372    /// Creates a new ApiError with the given status code and message.
373    pub fn api_error(status_code: u16, message: impl Into<String>) -> Self {
374        Self::ApiError {
375            status_code,
376            message: message.into(),
377        }
378    }
379
380    /// Creates a new RequestError with the given message.
381    pub fn request_error(message: impl Into<String>) -> Self {
382        Self::RequestError {
383            message: message.into(),
384        }
385    }
386
387    /// Creates a new ParseError with the given message.
388    pub fn parse_error(message: impl Into<String>) -> Self {
389        Self::ParseError {
390            message: message.into(),
391        }
392    }
393
394    pub(crate) fn api_error_with_context(
395        provider: &str,
396        path: &str,
397        status_code: u16,
398        body: &[u8],
399    ) -> Self {
400        let message =
401            format_response_context(provider, path, format_args!("status={status_code}"), body);
402        Self::api_error(status_code, message)
403    }
404
405    pub(crate) fn parse_error_with_context(
406        provider: &str,
407        path: &str,
408        error: &serde_json::Error,
409        body: &[u8],
410    ) -> Self {
411        let message =
412            format_response_context(provider, path, format_args!("parse_error={error}"), body);
413        Self::parse_error(message)
414    }
415
416    pub(crate) fn parse_error_with_details(
417        provider: &str,
418        path: &str,
419        details: impl fmt::Display,
420        body: &[u8],
421    ) -> Self {
422        let message = format_response_context(provider, path, details, body);
423        Self::parse_error(message)
424    }
425}
426
427impl From<crate::http_client::Error> for ModelListingError {
428    fn from(e: crate::http_client::Error) -> Self {
429        Self::request_error(e.to_string())
430    }
431}
432
433impl From<http::Error> for ModelListingError {
434    fn from(e: http::Error) -> Self {
435        Self::request_error(e.to_string())
436    }
437}
438
439impl From<serde_json::Error> for ModelListingError {
440    fn from(e: serde_json::Error) -> Self {
441        Self::parse_error(e.to_string())
442    }
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn test_model_from_id() {
451        let model = Model::from_id("gpt-4");
452        assert_eq!(model.id, "gpt-4");
453        assert_eq!(model.name, None);
454        assert_eq!(model.description, None);
455        assert_eq!(model.r#type, None);
456        assert_eq!(model.created_at, None);
457        assert_eq!(model.owned_by, None);
458        assert_eq!(model.context_length, None);
459    }
460
461    #[test]
462    fn test_model_new() {
463        let model = Model::new("gpt-4", "GPT-4");
464        assert_eq!(model.id, "gpt-4");
465        assert_eq!(model.name, Some("GPT-4".to_string()));
466    }
467
468    #[test]
469    fn test_model_display_name() {
470        let model_with_name = Model::new("gpt-4", "GPT-4");
471        assert_eq!(model_with_name.display_name(), "GPT-4");
472
473        let model_without_name = Model::from_id("gpt-4");
474        assert_eq!(model_without_name.display_name(), "gpt-4");
475    }
476
477    #[test]
478    fn test_model_display() {
479        let model = Model::new("gpt-4", "GPT-4");
480        assert_eq!(format!("{}", model), "GPT-4");
481    }
482
483    #[test]
484    fn test_model_list_new() {
485        let list = ModelList::new(vec![Model::from_id("gpt-4")]);
486        assert_eq!(list.len(), 1);
487    }
488
489    #[test]
490    fn test_model_list_empty() {
491        let list = ModelList::new(vec![]);
492        assert!(list.is_empty());
493        assert_eq!(list.len(), 0);
494    }
495
496    #[test]
497    fn test_model_list_iter() {
498        let list = ModelList::new(vec![
499            Model::from_id("gpt-4"),
500            Model::from_id("gpt-3.5-turbo"),
501        ]);
502        let models: Vec<_> = list.iter().collect();
503        assert_eq!(models.len(), 2);
504    }
505
506    #[test]
507    fn test_model_list_into_iter() {
508        let list = ModelList::new(vec![
509            Model::from_id("gpt-4"),
510            Model::from_id("gpt-3.5-turbo"),
511        ]);
512        let models: Vec<_> = list.into_iter().collect();
513        assert_eq!(models.len(), 2);
514    }
515
516    #[test]
517    fn test_model_listing_error_display() {
518        let error = ModelListingError::api_error(404, "Not found");
519        assert_eq!(error.to_string(), "API error (status 404): Not found");
520
521        let error = ModelListingError::request_error("Connection failed");
522        assert_eq!(error.to_string(), "Request error: Connection failed");
523
524        let error = ModelListingError::parse_error("Invalid JSON");
525        assert_eq!(error.to_string(), "Parse error: Invalid JSON");
526
527        let error = ModelListingError::AuthError {
528            message: "Invalid API key".to_string(),
529        };
530        assert_eq!(error.to_string(), "Authentication error: Invalid API key");
531    }
532
533    #[test]
534    fn test_model_serde() {
535        let model = Model {
536            id: "gpt-4".to_string(),
537            name: Some("GPT-4".to_string()),
538            description: None,
539            r#type: Some("chat".to_string()),
540            created_at: Some(1677610600),
541            owned_by: Some("openai".to_string()),
542            context_length: Some(8192),
543            max_output_tokens: Some(4096),
544        };
545
546        let json = serde_json::to_string(&model).unwrap();
547        assert!(json.contains("gpt-4"));
548        assert!(json.contains("GPT-4"));
549
550        let deserialized: Model = serde_json::from_str(&json).unwrap();
551        assert_eq!(deserialized.id, "gpt-4");
552        assert_eq!(deserialized.name, Some("GPT-4".to_string()));
553    }
554
555    #[test]
556    fn test_model_list_serde() {
557        let list = ModelList {
558            data: vec![Model::from_id("gpt-4")],
559        };
560
561        let json = serde_json::to_string(&list).unwrap();
562        assert!(json.contains("gpt-4"));
563
564        let deserialized: ModelList = serde_json::from_str(&json).unwrap();
565        assert_eq!(deserialized.len(), 1);
566    }
567
568    #[test]
569    fn test_model_listing_error_serde() {
570        let error = ModelListingError::api_error(404, "Not found");
571
572        let json = serde_json::to_string(&error).unwrap();
573        assert!(json.contains("ApiError"));
574
575        let deserialized: ModelListingError = serde_json::from_str(&json).unwrap();
576        match deserialized {
577            ModelListingError::ApiError {
578                status_code,
579                message,
580            } => {
581                assert_eq!(status_code, 404);
582                assert_eq!(message, "Not found");
583            }
584            _ => panic!("Expected ApiError"),
585        }
586    }
587
588    #[test]
589    fn test_format_response_body_preview_without_truncation() {
590        let preview = format_response_body_preview(br#"{"ok":true}"#);
591        assert_eq!(preview, r#"{"ok":true}"#);
592    }
593
594    #[test]
595    fn test_format_response_body_preview_with_truncation() {
596        let body = vec![b'a'; RESPONSE_BODY_PREVIEW_LIMIT + 3];
597        let preview = format_response_body_preview(&body);
598
599        assert!(preview.starts_with(&"a".repeat(RESPONSE_BODY_PREVIEW_LIMIT)));
600        assert!(preview.ends_with("\n...<truncated 3 bytes>"));
601    }
602
603    #[test]
604    fn test_api_error_with_context_includes_provider_path_and_preview() {
605        let error = ModelListingError::api_error_with_context(
606            "Gemini",
607            "/v1beta/models?pageSize=1000",
608            500,
609            br#"{"error":"boom"}"#,
610        );
611
612        match error {
613            ModelListingError::ApiError {
614                status_code,
615                message,
616            } => {
617                assert_eq!(status_code, 500);
618                assert!(message.contains("provider=Gemini"));
619                assert!(message.contains("path=/v1beta/models?pageSize=1000"));
620                assert!(message.contains("status=500"));
621                assert!(message.contains(r#"{"error":"boom"}"#));
622            }
623            _ => panic!("Expected ApiError"),
624        }
625    }
626
627    #[test]
628    fn test_parse_error_with_context_includes_parse_error_and_preview() {
629        let body = br#"{"models":[{"displayName":"broken"}]}"#;
630        let parse_error = serde_json::from_slice::<serde_json::Value>(b"{")
631            .expect_err("expected malformed JSON to fail");
632        let error = ModelListingError::parse_error_with_context(
633            "Gemini",
634            "/v1beta/models?pageSize=1000",
635            &parse_error,
636            body,
637        );
638
639        match error {
640            ModelListingError::ParseError { message } => {
641                assert!(message.contains("provider=Gemini"));
642                assert!(message.contains("path=/v1beta/models?pageSize=1000"));
643                assert!(message.contains("parse_error=EOF while parsing an object"));
644                assert!(message.contains(r#"{"models":[{"displayName":"broken"}]}"#));
645            }
646            _ => panic!("Expected ParseError"),
647        }
648    }
649}