1use serde::{Deserialize, Serialize};
33
34use crate::types::ModelId;
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[non_exhaustive]
39pub struct ModelInfo {
40 pub id: ModelId,
42 #[serde(default)]
44 pub display_name: String,
45 #[serde(default)]
47 pub created_at: String,
48 #[serde(rename = "type", default = "default_model_kind")]
50 pub kind: String,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub max_tokens: Option<u64>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub max_input_tokens: Option<u64>,
58 #[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#[derive(Debug, Clone, Default, Serialize)]
72#[non_exhaustive]
73pub struct ListModelsParams {
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub before_id: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub after_id: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub limit: Option<u32>,
83}
84
85impl ListModelsParams {
86 #[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 #[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 #[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 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 pub async fn list(&self, params: ListModelsParams) -> Result<Paginated<ModelInfo>> {
133 let params_ref = ¶ms;
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 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 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 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 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}