Skip to main content

rig_core/model/
listing.rs

1//! Provider model metadata and listing interfaces.
2//!
3//! ```
4//! use rig_core::model::{ModelInfo, ModelList};
5//!
6//! let models = ModelList::new(vec![ModelInfo::new("example", "Example model")]);
7//! assert_eq!(models.len(), 1);
8//! ```
9
10use crate::error::ProviderError;
11use serde::{Deserialize, Serialize};
12use std::fmt;
13
14/// Provider-advertised model identifier and optional metadata.
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub struct ModelInfo {
17    /// The unique identifier for the model (required)
18    pub id: String,
19
20    /// A human-readable name for the model
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub name: Option<String>,
23
24    /// A detailed description of the model's capabilities
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub description: Option<String>,
27
28    /// The type of model (e.g., "chat", "completion", "embedding")
29    #[serde(skip_serializing_if = "Option::is_none")]
30    #[serde(rename = "type")]
31    pub r#type: Option<String>,
32
33    /// Timestamp when the model was created (Unix epoch)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub created_at: Option<u64>,
36
37    /// The organization or entity that owns the model
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub owned_by: Option<String>,
40
41    /// The maximum context window size for the model
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub context_length: Option<u32>,
44
45    /// Provider-reported output-token ceiling, or `None` when unreported.
46    /// Distinct from context length and not automatically applied to requests.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub max_output_tokens: Option<u32>,
49}
50
51impl ModelInfo {
52    /// Creates a model with an ID and display name; other metadata is absent.
53    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
54        Self {
55            id: id.into(),
56            name: Some(name.into()),
57            description: None,
58            r#type: None,
59            created_at: None,
60            owned_by: None,
61            context_length: None,
62            max_output_tokens: None,
63        }
64    }
65
66    /// Creates a model with an ID and no optional metadata.
67    pub fn from_id(id: impl Into<String>) -> Self {
68        Self {
69            id: id.into(),
70            name: None,
71            description: None,
72            r#type: None,
73            created_at: None,
74            owned_by: None,
75            context_length: None,
76            max_output_tokens: None,
77        }
78    }
79
80    /// Returns the name when present, otherwise the ID.
81    pub fn display_name(&self) -> &str {
82        self.name.as_ref().unwrap_or(&self.id)
83    }
84}
85
86impl fmt::Display for ModelInfo {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        write!(f, "{}", self.display_name())
89    }
90}
91
92/// Ordered provider model entries. May represent one page or an aggregated listing.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ModelList {
95    /// Model entries in returned order.
96    pub data: Vec<ModelInfo>,
97}
98
99impl ModelList {
100    /// Wraps model entries without sorting or deduplicating them.
101    pub fn new(data: Vec<ModelInfo>) -> Self {
102        Self { data }
103    }
104
105    /// Returns whether the list has no entries.
106    pub fn is_empty(&self) -> bool {
107        self.data.is_empty()
108    }
109
110    /// Returns the number of entries.
111    pub fn len(&self) -> usize {
112        self.data.len()
113    }
114
115    /// Borrows entries in list order.
116    pub fn iter(&self) -> std::slice::Iter<'_, ModelInfo> {
117        self.data.iter()
118    }
119}
120
121impl IntoIterator for ModelList {
122    type Item = ModelInfo;
123    type IntoIter = std::vec::IntoIter<ModelInfo>;
124
125    fn into_iter(self) -> Self::IntoIter {
126        self.data.into_iter()
127    }
128}
129
130impl<'a> IntoIterator for &'a ModelList {
131    type Item = &'a ModelInfo;
132    type IntoIter = std::slice::Iter<'a, ModelInfo>;
133
134    fn into_iter(self) -> Self::IntoIter {
135        self.data.iter()
136    }
137}
138
139const RESPONSE_BODY_PREVIEW_LIMIT: usize = 2048;
140
141fn format_response_body_preview(body: &[u8]) -> String {
142    let preview_len = body.len().min(RESPONSE_BODY_PREVIEW_LIMIT);
143    let preview_bytes = body.get(..preview_len).unwrap_or(body);
144    let mut preview = String::from_utf8_lossy(preview_bytes).into_owned();
145
146    if body.len() > RESPONSE_BODY_PREVIEW_LIMIT {
147        preview.push_str(&format!(
148            "\n...<truncated {} bytes>",
149            body.len() - RESPONSE_BODY_PREVIEW_LIMIT
150        ));
151    }
152
153    preview
154}
155
156fn format_response_context(
157    provider: &str,
158    path: &str,
159    details: impl fmt::Display,
160    body: &[u8],
161) -> String {
162    format!(
163        "provider={provider}\npath={path}\n{details}\nbody_bytes={}\nresponse_body_preview:\n{}",
164        body.len(),
165        format_response_body_preview(body)
166    )
167}
168
169/// A listing page that did not parse, with the request context and a bounded
170/// preview of the body.
171pub(crate) fn parse_error(
172    provider: &str,
173    path: &str,
174    details: impl fmt::Display,
175    body: &[u8],
176) -> ProviderError {
177    ProviderError::Response(format_response_context(provider, path, details, body))
178}
179
180/// Adds the provider and request path to a failed listing: a preserved reply
181/// records them as its route, and a decode failure names them in its message.
182pub(crate) fn with_route(error: ProviderError, provider: &str, path: &str) -> ProviderError {
183    match error {
184        ProviderError::ProviderResponse(mut response) => {
185            response.route = Some(format!("provider={provider} path={path}"));
186            ProviderError::ProviderResponse(response)
187        }
188        ProviderError::Json(error) => parse_error(
189            provider,
190            path,
191            format_args!("parse_error"),
192            error.to_string().as_bytes(),
193        ),
194        ProviderError::Response(message) => parse_error(
195            provider,
196            path,
197            format_args!("parse_error"),
198            message.as_bytes(),
199        ),
200        other => other,
201    }
202}
203
204#[cfg(test)]
205mod tests;