1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct Model {
51 pub id: String,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub name: Option<String>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub description: Option<String>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 #[serde(rename = "type")]
65 pub r#type: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub created_at: Option<u64>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub owned_by: Option<String>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub context_length: Option<u32>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
91 pub max_output_tokens: Option<u32>,
92}
93
94impl Model {
95 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ModelList {
205 pub data: Vec<Model>,
207}
208
209impl ModelList {
210 pub fn new(data: Vec<Model>) -> Self {
228 Self { data }
229 }
230
231 pub fn is_empty(&self) -> bool {
245 self.data.is_empty()
246 }
247
248 pub fn len(&self) -> usize {
262 self.data.len()
263 }
264
265 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#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
309pub enum ModelListingError {
310 #[error("API error (status {status_code}): {message}")]
312 ApiError {
313 status_code: u16,
315 message: String,
317 },
318
319 #[error("Request error: {message}")]
321 RequestError {
322 message: String,
324 },
325
326 #[error("Parse error: {message}")]
328 ParseError {
329 message: String,
331 },
332
333 #[error("Authentication error: {message}")]
335 AuthError {
336 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 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 pub fn request_error(message: impl Into<String>) -> Self {
382 Self::RequestError {
383 message: message.into(),
384 }
385 }
386
387 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}