1use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::SdkError;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
24pub struct ModelId {
25 pub provider: String,
27 pub model: String,
29}
30
31impl ModelId {
32 pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
34 Self {
35 provider: provider.into(),
36 model: model.into(),
37 }
38 }
39}
40
41impl fmt::Display for ModelId {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{}::{}", self.provider, self.model)
44 }
45}
46
47impl FromStr for ModelId {
48 type Err = SdkError;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 let (provider, model) = s.split_once("::").ok_or_else(|| {
55 SdkError::Domain(format!(
56 "model id {s:?} must be fully qualified as provider::model"
57 ))
58 })?;
59 if provider.is_empty() || model.is_empty() {
60 return Err(SdkError::Domain(format!(
61 "model id {s:?} has an empty provider or model half"
62 )));
63 }
64 Ok(Self::new(provider, model))
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn renders_and_parses_fully_qualified_ids() {
74 let id = ModelId::new("anthropic", "claude-opus-5");
75 assert_eq!(id.to_string(), "anthropic::claude-opus-5");
76 assert_eq!("anthropic::claude-opus-5".parse::<ModelId>().unwrap(), id);
77 }
78
79 #[test]
80 fn model_halves_may_contain_separators() {
81 let id: ModelId = "vertex::publishers/google/models/gemini".parse().unwrap();
83 assert_eq!(id.provider, "vertex");
84 assert_eq!(id.model, "publishers/google/models/gemini");
85 }
86
87 #[test]
88 fn rejects_bare_names_and_empty_halves() {
89 assert!("claude-opus-5".parse::<ModelId>().is_err());
90 assert!("::model".parse::<ModelId>().is_err());
91 assert!("provider::".parse::<ModelId>().is_err());
92 }
93}