Skip to main content

claude_api/models/
mod.rs

1//! The Models API.
2//!
3//! Discover what models are available to your API key, with their
4//! capability matrix and per-model token limits.
5//!
6//! # Endpoints
7//!
8//! | Method | Path | Function |
9//! |---|---|---|
10//! | `GET` | `/v1/models` | [`Models::list`] (paginated) |
11//! | `GET` | `/v1/models` | [`Models::list_all`] (auto-paginates) |
12//! | `GET` | `/v1/models/{id}` | [`Models::get`] |
13//!
14//! # Quick start
15//!
16//! ```no_run
17//! use claude_api::{Client, models::ListModelsParams};
18//! # async fn run() -> Result<(), claude_api::Error> {
19//! let client = Client::new("sk-ant-...");
20//!
21//! // Iterate the full set transparently:
22//! for model in client.models().list_all().await? {
23//!     println!("{}: {}", model.id.as_str(), model.display_name);
24//! }
25//!
26//! // Or fetch one by ID:
27//! let m = client.models().get("claude-sonnet-4-6").await?;
28//! println!("{} (max input: {:?})", m.display_name, m.max_input_tokens);
29//! # Ok(()) }
30//! ```
31
32use serde::{Deserialize, Serialize};
33
34use crate::types::ModelId;
35
36/// Metadata for a single model returned by the Models API.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[non_exhaustive]
39pub struct ModelInfo {
40    /// Stable model identifier (e.g. `claude-opus-4-7`).
41    pub id: ModelId,
42    /// Human-readable display name.
43    #[serde(default)]
44    pub display_name: String,
45    /// Creation timestamp (ISO-8601 string).
46    #[serde(default)]
47    pub created_at: String,
48    /// Wire `type` discriminant; always `"model"`.
49    #[serde(rename = "type", default = "default_model_kind")]
50    pub kind: String,
51    /// Maximum total tokens (input + output) the model can produce in
52    /// a single response.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub max_tokens: Option<u64>,
55    /// Maximum input tokens the model can accept.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub max_input_tokens: Option<u64>,
58    /// Capability matrix: which features (citations, code execution,
59    /// thinking, image input, etc.) the model supports and at what
60    /// level. Currently preserved as raw JSON; promote to a typed
61    /// `BetaModelCapabilities` struct in a future revision.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub capabilities: Option<serde_json::Value>,
64}
65
66fn default_model_kind() -> String {
67    "model".to_owned()
68}
69
70/// Query parameters for `GET /v1/models`.
71#[derive(Debug, Clone, Default, Serialize)]
72#[non_exhaustive]
73pub struct ListModelsParams {
74    /// Cursor for backward pagination: page items before this `id`.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub before_id: Option<String>,
77    /// Cursor for forward pagination: page items after this `id`.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub after_id: Option<String>,
80    /// Page size (server-defaulted if omitted; 1..=1000).
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub limit: Option<u32>,
83}
84
85impl ListModelsParams {
86    /// Set the `after_id` cursor (forward paging).
87    #[must_use]
88    pub fn after_id(mut self, id: impl Into<String>) -> Self {
89        self.after_id = Some(id.into());
90        self
91    }
92
93    /// Set the `before_id` cursor (backward paging).
94    #[must_use]
95    pub fn before_id(mut self, id: impl Into<String>) -> Self {
96        self.before_id = Some(id.into());
97        self
98    }
99
100    /// Set the page size.
101    #[must_use]
102    pub fn limit(mut self, limit: u32) -> Self {
103        self.limit = Some(limit);
104        self
105    }
106}
107
108#[cfg(feature = "async")]
109#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
110pub use api::Models;
111
112#[cfg(feature = "async")]
113mod api {
114    use super::{ListModelsParams, ModelInfo};
115    use crate::client::Client;
116    use crate::error::Result;
117    use crate::pagination::Paginated;
118
119    /// Namespace handle for the Models API.
120    ///
121    /// Obtained via [`Client::models`](crate::Client::models).
122    pub struct Models<'a> {
123        client: &'a Client,
124    }
125
126    impl<'a> Models<'a> {
127        pub(crate) fn new(client: &'a Client) -> Self {
128            Self { client }
129        }
130
131        /// Fetch one page of models.
132        pub async fn list(&self, params: ListModelsParams) -> Result<Paginated<ModelInfo>> {
133            let params_ref = &params;
134            self.client
135                .execute_with_retry(
136                    || {
137                        self.client
138                            .request_builder(reqwest::Method::GET, "/v1/models")
139                            .query(params_ref)
140                    },
141                    &[],
142                )
143                .await
144        }
145
146        /// Fetch all models, transparently paging until exhausted.
147        ///
148        /// Returns the full list as a single `Vec`. Use [`Self::list`] if
149        /// you need to control paging yourself (e.g. for backpressure).
150        pub async fn list_all(&self) -> Result<Vec<ModelInfo>> {
151            let mut all = Vec::new();
152            let mut params = ListModelsParams::default();
153            loop {
154                let page = self.list(params.clone()).await?;
155                let next_cursor = page.next_after().map(str::to_owned);
156                all.extend(page.data);
157                match next_cursor {
158                    Some(cursor) => params.after_id = Some(cursor),
159                    None => break,
160                }
161            }
162            Ok(all)
163        }
164
165        /// Fetch metadata for a single model by ID.
166        pub async fn get(&self, id: impl AsRef<str>) -> Result<ModelInfo> {
167            let path = format!("/v1/models/{}", id.as_ref());
168            self.client
169                .execute_with_retry(
170                    || self.client.request_builder(reqwest::Method::GET, &path),
171                    &[],
172                )
173                .await
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use pretty_assertions::assert_eq;
182    use serde_json::json;
183
184    #[test]
185    fn model_info_round_trips_with_known_fields() {
186        let raw = json!({
187            "type": "model",
188            "id": "claude-opus-4-7",
189            "display_name": "Claude Opus 4.7",
190            "created_at": "2025-12-01T00:00:00Z"
191        });
192        let m: ModelInfo = serde_json::from_value(raw.clone()).unwrap();
193        assert_eq!(m.id, ModelId::OPUS_4_7);
194        assert_eq!(m.display_name, "Claude Opus 4.7");
195        assert_eq!(m.created_at, "2025-12-01T00:00:00Z");
196        assert_eq!(m.kind, "model");
197        let v = serde_json::to_value(&m).unwrap();
198        assert_eq!(v, raw);
199    }
200
201    #[test]
202    fn model_info_kind_defaults_to_model_when_missing() {
203        let raw = json!({"id": "claude-x", "display_name": "X", "created_at": "2025"});
204        let m: ModelInfo = serde_json::from_value(raw).unwrap();
205        assert_eq!(m.kind, "model");
206    }
207
208    #[test]
209    fn list_models_params_default_serializes_to_empty_object() {
210        let p = ListModelsParams::default();
211        assert_eq!(serde_json::to_value(&p).unwrap(), json!({}));
212    }
213
214    #[test]
215    fn list_models_params_builder_methods() {
216        let p = ListModelsParams::default().after_id("abc").limit(50);
217        assert_eq!(p.after_id.as_deref(), Some("abc"));
218        assert_eq!(p.limit, Some(50));
219    }
220}
221
222#[cfg(all(test, feature = "async"))]
223mod api_tests {
224    use super::*;
225    use crate::client::Client;
226    use pretty_assertions::assert_eq;
227    use serde_json::json;
228    use wiremock::matchers::{method, path, query_param};
229    use wiremock::{Mock, MockServer, ResponseTemplate};
230
231    fn client_for(mock: &MockServer) -> Client {
232        Client::builder()
233            .api_key("sk-ant-test")
234            .base_url(mock.uri())
235            .build()
236            .unwrap()
237    }
238
239    fn page_body(ids: &[&str], has_more: bool) -> serde_json::Value {
240        let data: Vec<_> = ids
241            .iter()
242            .map(|id| {
243                json!({
244                    "type": "model",
245                    "id": id,
246                    "display_name": id,
247                    "created_at": "2025-01-01T00:00:00Z"
248                })
249            })
250            .collect();
251        json!({
252            "data": data,
253            "has_more": has_more,
254            "first_id": ids.first().unwrap_or(&""),
255            "last_id": ids.last().unwrap_or(&"")
256        })
257    }
258
259    #[tokio::test]
260    async fn list_returns_a_single_page() {
261        let mock = MockServer::start().await;
262        Mock::given(method("GET"))
263            .and(path("/v1/models"))
264            .respond_with(
265                ResponseTemplate::new(200)
266                    .set_body_json(page_body(&["claude-opus-4-7", "claude-sonnet-4-6"], false)),
267            )
268            .mount(&mock)
269            .await;
270
271        let client = client_for(&mock);
272        let page = client
273            .models()
274            .list(ListModelsParams::default())
275            .await
276            .unwrap();
277        assert_eq!(page.data.len(), 2);
278        assert_eq!(page.data[0].id, ModelId::OPUS_4_7);
279        assert!(!page.has_more);
280        assert_eq!(page.next_after(), None);
281    }
282
283    #[tokio::test]
284    async fn list_passes_pagination_query_params() {
285        let mock = MockServer::start().await;
286        Mock::given(method("GET"))
287            .and(path("/v1/models"))
288            .and(query_param("after_id", "claude-x"))
289            .and(query_param("limit", "10"))
290            .respond_with(ResponseTemplate::new(200).set_body_json(page_body(&[], false)))
291            .mount(&mock)
292            .await;
293
294        let client = client_for(&mock);
295        let _ = client
296            .models()
297            .list(ListModelsParams::default().after_id("claude-x").limit(10))
298            .await
299            .unwrap();
300    }
301
302    #[tokio::test]
303    async fn list_all_pages_through_results_and_concatenates() {
304        let mock = MockServer::start().await;
305        // First page: has_more = true, last_id = "claude-sonnet-4-6"
306        Mock::given(method("GET"))
307            .and(path("/v1/models"))
308            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
309                "data": [
310                    {"type": "model", "id": "claude-opus-4-7", "display_name": "O", "created_at": "x"},
311                    {"type": "model", "id": "claude-sonnet-4-6", "display_name": "S", "created_at": "x"}
312                ],
313                "has_more": true,
314                "first_id": "claude-opus-4-7",
315                "last_id": "claude-sonnet-4-6"
316            })))
317            .up_to_n_times(1)
318            .mount(&mock)
319            .await;
320        // Second page: has_more = false. Wiremock must see after_id=claude-sonnet-4-6.
321        Mock::given(method("GET"))
322            .and(path("/v1/models"))
323            .and(query_param("after_id", "claude-sonnet-4-6"))
324            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
325                "data": [
326                    {"type": "model", "id": "claude-haiku-4-5-20251001", "display_name": "H", "created_at": "x"}
327                ],
328                "has_more": false,
329                "first_id": "claude-haiku-4-5-20251001",
330                "last_id": "claude-haiku-4-5-20251001"
331            })))
332            .mount(&mock)
333            .await;
334
335        let client = client_for(&mock);
336        let all = client.models().list_all().await.unwrap();
337        assert_eq!(all.len(), 3);
338        assert_eq!(all[0].id, ModelId::OPUS_4_7);
339        assert_eq!(all[1].id, ModelId::SONNET_4_6);
340        assert_eq!(all[2].id, ModelId::HAIKU_4_5);
341    }
342
343    #[tokio::test]
344    async fn get_fetches_a_single_model_by_id() {
345        let mock = MockServer::start().await;
346        Mock::given(method("GET"))
347            .and(path("/v1/models/claude-opus-4-7"))
348            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
349                "type": "model",
350                "id": "claude-opus-4-7",
351                "display_name": "Claude Opus 4.7",
352                "created_at": "2025-12-01T00:00:00Z"
353            })))
354            .mount(&mock)
355            .await;
356
357        let client = client_for(&mock);
358        let m = client.models().get("claude-opus-4-7").await.unwrap();
359        assert_eq!(m.id, ModelId::OPUS_4_7);
360        assert_eq!(m.display_name, "Claude Opus 4.7");
361    }
362
363    #[tokio::test]
364    async fn get_propagates_404_as_api_error() {
365        let mock = MockServer::start().await;
366        Mock::given(method("GET"))
367            .and(path("/v1/models/nope"))
368            .respond_with(ResponseTemplate::new(404).set_body_json(json!({
369                "type": "error",
370                "error": {"type": "not_found_error", "message": "no such model"}
371            })))
372            .mount(&mock)
373            .await;
374
375        let client = client_for(&mock);
376        let err = client.models().get("nope").await.unwrap_err();
377        assert_eq!(err.status(), Some(http::StatusCode::NOT_FOUND));
378    }
379}