1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub struct Model {
49 pub id: String,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub name: Option<String>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub description: Option<String>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 #[serde(rename = "type")]
63 pub r#type: Option<String>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub created_at: Option<u64>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub owned_by: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub context_length: Option<u32>,
76}
77
78impl Model {
79 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
96 Self {
97 id: id.into(),
98 name: Some(name.into()),
99 description: None,
100 r#type: None,
101 created_at: None,
102 owned_by: None,
103 context_length: None,
104 }
105 }
106
107 pub fn from_id(id: impl Into<String>) -> Self {
123 Self {
124 id: id.into(),
125 name: None,
126 description: None,
127 r#type: None,
128 created_at: None,
129 owned_by: None,
130 context_length: None,
131 }
132 }
133
134 pub fn display_name(&self) -> &str {
151 self.name.as_ref().unwrap_or(&self.id)
152 }
153}
154
155impl fmt::Display for Model {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "{}", self.display_name())
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ModelList {
187 pub data: Vec<Model>,
189}
190
191impl ModelList {
192 pub fn new(data: Vec<Model>) -> Self {
210 Self { data }
211 }
212
213 pub fn is_empty(&self) -> bool {
227 self.data.is_empty()
228 }
229
230 pub fn len(&self) -> usize {
244 self.data.len()
245 }
246
247 pub fn iter(&self) -> std::slice::Iter<'_, Model> {
264 self.data.iter()
265 }
266}
267
268impl IntoIterator for ModelList {
269 type Item = Model;
270 type IntoIter = std::vec::IntoIter<Model>;
271
272 fn into_iter(self) -> Self::IntoIter {
273 self.data.into_iter()
274 }
275}
276
277impl<'a> IntoIterator for &'a ModelList {
278 type Item = &'a Model;
279 type IntoIter = std::slice::Iter<'a, Model>;
280
281 fn into_iter(self) -> Self::IntoIter {
282 self.data.iter()
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
291pub enum ModelListingError {
292 ApiError {
294 status_code: u16,
296 message: String,
298 },
299
300 RequestError {
302 message: String,
304 },
305
306 ParseError {
308 message: String,
310 },
311
312 AuthError {
314 message: String,
316 },
317
318 RateLimitError {
320 message: String,
322 },
323
324 ServiceUnavailable {
326 message: String,
328 },
329
330 UnknownError {
332 message: String,
334 },
335}
336
337const RESPONSE_BODY_PREVIEW_LIMIT: usize = 2048;
338
339fn format_response_body_preview(body: &[u8]) -> String {
340 let preview_len = body.len().min(RESPONSE_BODY_PREVIEW_LIMIT);
341 let mut preview = String::from_utf8_lossy(&body[..preview_len]).into_owned();
342
343 if body.len() > RESPONSE_BODY_PREVIEW_LIMIT {
344 preview.push_str(&format!(
345 "\n...<truncated {} bytes>",
346 body.len() - RESPONSE_BODY_PREVIEW_LIMIT
347 ));
348 }
349
350 preview
351}
352
353fn format_response_context(
354 provider: &str,
355 path: &str,
356 details: impl fmt::Display,
357 body: &[u8],
358) -> String {
359 format!(
360 "provider={provider}\npath={path}\n{details}\nbody_bytes={}\nresponse_body_preview:\n{}",
361 body.len(),
362 format_response_body_preview(body)
363 )
364}
365
366impl ModelListingError {
367 pub fn api_error(status_code: u16, message: impl Into<String>) -> Self {
369 Self::ApiError {
370 status_code,
371 message: message.into(),
372 }
373 }
374
375 pub fn request_error(message: impl Into<String>) -> Self {
377 Self::RequestError {
378 message: message.into(),
379 }
380 }
381
382 pub fn parse_error(message: impl Into<String>) -> Self {
384 Self::ParseError {
385 message: message.into(),
386 }
387 }
388
389 pub(crate) fn api_error_with_context(
390 provider: &str,
391 path: &str,
392 status_code: u16,
393 body: &[u8],
394 ) -> Self {
395 let message =
396 format_response_context(provider, path, format_args!("status={status_code}"), body);
397 Self::api_error(status_code, message)
398 }
399
400 pub(crate) fn parse_error_with_context(
401 provider: &str,
402 path: &str,
403 error: &serde_json::Error,
404 body: &[u8],
405 ) -> Self {
406 let message =
407 format_response_context(provider, path, format_args!("parse_error={error}"), body);
408 Self::parse_error(message)
409 }
410
411 pub(crate) fn parse_error_with_details(
412 provider: &str,
413 path: &str,
414 details: impl fmt::Display,
415 body: &[u8],
416 ) -> Self {
417 let message = format_response_context(provider, path, details, body);
418 Self::parse_error(message)
419 }
420
421 pub fn auth_error(message: impl Into<String>) -> Self {
423 Self::AuthError {
424 message: message.into(),
425 }
426 }
427
428 pub fn rate_limit_error(message: impl Into<String>) -> Self {
430 Self::RateLimitError {
431 message: message.into(),
432 }
433 }
434
435 pub fn service_unavailable(message: impl Into<String>) -> Self {
437 Self::ServiceUnavailable {
438 message: message.into(),
439 }
440 }
441
442 pub fn unknown_error(message: impl Into<String>) -> Self {
444 Self::UnknownError {
445 message: message.into(),
446 }
447 }
448}
449
450impl fmt::Display for ModelListingError {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 match self {
453 Self::ApiError {
454 status_code,
455 message,
456 } => write!(f, "API error (status {}): {}", status_code, message),
457 Self::RequestError { message } => write!(f, "Request error: {}", message),
458 Self::ParseError { message } => write!(f, "Parse error: {}", message),
459 Self::AuthError { message } => write!(f, "Authentication error: {}", message),
460 Self::RateLimitError { message } => write!(f, "Rate limit error: {}", message),
461 Self::ServiceUnavailable { message } => write!(f, "Service unavailable: {}", message),
462 Self::UnknownError { message } => write!(f, "Unknown error: {}", message),
463 }
464 }
465}
466
467impl std::error::Error for ModelListingError {}
468
469impl From<crate::http_client::Error> for ModelListingError {
470 fn from(e: crate::http_client::Error) -> Self {
471 Self::request_error(e.to_string())
472 }
473}
474
475impl From<http::Error> for ModelListingError {
476 fn from(e: http::Error) -> Self {
477 Self::request_error(e.to_string())
478 }
479}
480
481impl From<serde_json::Error> for ModelListingError {
482 fn from(e: serde_json::Error) -> Self {
483 Self::parse_error(e.to_string())
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn test_model_from_id() {
493 let model = Model::from_id("gpt-4");
494 assert_eq!(model.id, "gpt-4");
495 assert_eq!(model.name, None);
496 assert_eq!(model.description, None);
497 assert_eq!(model.r#type, None);
498 assert_eq!(model.created_at, None);
499 assert_eq!(model.owned_by, None);
500 assert_eq!(model.context_length, None);
501 }
502
503 #[test]
504 fn test_model_new() {
505 let model = Model::new("gpt-4", "GPT-4");
506 assert_eq!(model.id, "gpt-4");
507 assert_eq!(model.name, Some("GPT-4".to_string()));
508 }
509
510 #[test]
511 fn test_model_display_name() {
512 let model_with_name = Model::new("gpt-4", "GPT-4");
513 assert_eq!(model_with_name.display_name(), "GPT-4");
514
515 let model_without_name = Model::from_id("gpt-4");
516 assert_eq!(model_without_name.display_name(), "gpt-4");
517 }
518
519 #[test]
520 fn test_model_display() {
521 let model = Model::new("gpt-4", "GPT-4");
522 assert_eq!(format!("{}", model), "GPT-4");
523 }
524
525 #[test]
526 fn test_model_list_new() {
527 let list = ModelList::new(vec![Model::from_id("gpt-4")]);
528 assert_eq!(list.len(), 1);
529 }
530
531 #[test]
532 fn test_model_list_empty() {
533 let list = ModelList::new(vec![]);
534 assert!(list.is_empty());
535 assert_eq!(list.len(), 0);
536 }
537
538 #[test]
539 fn test_model_list_iter() {
540 let list = ModelList::new(vec![
541 Model::from_id("gpt-4"),
542 Model::from_id("gpt-3.5-turbo"),
543 ]);
544 let models: Vec<_> = list.iter().collect();
545 assert_eq!(models.len(), 2);
546 }
547
548 #[test]
549 fn test_model_list_into_iter() {
550 let list = ModelList::new(vec![
551 Model::from_id("gpt-4"),
552 Model::from_id("gpt-3.5-turbo"),
553 ]);
554 let models: Vec<_> = list.into_iter().collect();
555 assert_eq!(models.len(), 2);
556 }
557
558 #[test]
559 fn test_model_listing_error_display() {
560 let error = ModelListingError::api_error(404, "Not found");
561 assert_eq!(error.to_string(), "API error (status 404): Not found");
562
563 let error = ModelListingError::request_error("Connection failed");
564 assert_eq!(error.to_string(), "Request error: Connection failed");
565
566 let error = ModelListingError::parse_error("Invalid JSON");
567 assert_eq!(error.to_string(), "Parse error: Invalid JSON");
568
569 let error = ModelListingError::auth_error("Invalid API key");
570 assert_eq!(error.to_string(), "Authentication error: Invalid API key");
571
572 let error = ModelListingError::rate_limit_error("Too many requests");
573 assert_eq!(error.to_string(), "Rate limit error: Too many requests");
574
575 let error = ModelListingError::service_unavailable("Maintenance mode");
576 assert_eq!(error.to_string(), "Service unavailable: Maintenance mode");
577
578 let error = ModelListingError::unknown_error("Something went wrong");
579 assert_eq!(error.to_string(), "Unknown error: Something went wrong");
580 }
581
582 #[test]
583 fn test_model_serde() {
584 let model = Model {
585 id: "gpt-4".to_string(),
586 name: Some("GPT-4".to_string()),
587 description: None,
588 r#type: Some("chat".to_string()),
589 created_at: Some(1677610600),
590 owned_by: Some("openai".to_string()),
591 context_length: Some(8192),
592 };
593
594 let json = serde_json::to_string(&model).unwrap();
595 assert!(json.contains("gpt-4"));
596 assert!(json.contains("GPT-4"));
597
598 let deserialized: Model = serde_json::from_str(&json).unwrap();
599 assert_eq!(deserialized.id, "gpt-4");
600 assert_eq!(deserialized.name, Some("GPT-4".to_string()));
601 }
602
603 #[test]
604 fn test_model_list_serde() {
605 let list = ModelList {
606 data: vec![Model::from_id("gpt-4")],
607 };
608
609 let json = serde_json::to_string(&list).unwrap();
610 assert!(json.contains("gpt-4"));
611
612 let deserialized: ModelList = serde_json::from_str(&json).unwrap();
613 assert_eq!(deserialized.len(), 1);
614 }
615
616 #[test]
617 fn test_model_listing_error_serde() {
618 let error = ModelListingError::api_error(404, "Not found");
619
620 let json = serde_json::to_string(&error).unwrap();
621 assert!(json.contains("ApiError"));
622
623 let deserialized: ModelListingError = serde_json::from_str(&json).unwrap();
624 match deserialized {
625 ModelListingError::ApiError {
626 status_code,
627 message,
628 } => {
629 assert_eq!(status_code, 404);
630 assert_eq!(message, "Not found");
631 }
632 _ => panic!("Expected ApiError"),
633 }
634 }
635
636 #[test]
637 fn test_format_response_body_preview_without_truncation() {
638 let preview = format_response_body_preview(br#"{"ok":true}"#);
639 assert_eq!(preview, r#"{"ok":true}"#);
640 }
641
642 #[test]
643 fn test_format_response_body_preview_with_truncation() {
644 let body = vec![b'a'; RESPONSE_BODY_PREVIEW_LIMIT + 3];
645 let preview = format_response_body_preview(&body);
646
647 assert!(preview.starts_with(&"a".repeat(RESPONSE_BODY_PREVIEW_LIMIT)));
648 assert!(preview.ends_with("\n...<truncated 3 bytes>"));
649 }
650
651 #[test]
652 fn test_api_error_with_context_includes_provider_path_and_preview() {
653 let error = ModelListingError::api_error_with_context(
654 "Gemini",
655 "/v1beta/models?pageSize=1000",
656 500,
657 br#"{"error":"boom"}"#,
658 );
659
660 match error {
661 ModelListingError::ApiError {
662 status_code,
663 message,
664 } => {
665 assert_eq!(status_code, 500);
666 assert!(message.contains("provider=Gemini"));
667 assert!(message.contains("path=/v1beta/models?pageSize=1000"));
668 assert!(message.contains("status=500"));
669 assert!(message.contains(r#"{"error":"boom"}"#));
670 }
671 _ => panic!("Expected ApiError"),
672 }
673 }
674
675 #[test]
676 fn test_parse_error_with_context_includes_parse_error_and_preview() {
677 let body = br#"{"models":[{"displayName":"broken"}]}"#;
678 let parse_error = serde_json::from_slice::<serde_json::Value>(b"{")
679 .expect_err("expected malformed JSON to fail");
680 let error = ModelListingError::parse_error_with_context(
681 "Gemini",
682 "/v1beta/models?pageSize=1000",
683 &parse_error,
684 body,
685 );
686
687 match error {
688 ModelListingError::ParseError { message } => {
689 assert!(message.contains("provider=Gemini"));
690 assert!(message.contains("path=/v1beta/models?pageSize=1000"));
691 assert!(message.contains("parse_error=EOF while parsing an object"));
692 assert!(message.contains(r#"{"models":[{"displayName":"broken"}]}"#));
693 }
694 _ => panic!("Expected ParseError"),
695 }
696 }
697}