use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::error::SdkError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ModelId {
pub provider: String,
pub model: String,
}
impl ModelId {
pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
Self {
provider: provider.into(),
model: model.into(),
}
}
}
impl fmt::Display for ModelId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}::{}", self.provider, self.model)
}
}
impl FromStr for ModelId {
type Err = SdkError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (provider, model) = s.split_once("::").ok_or_else(|| {
SdkError::Domain(format!(
"model id {s:?} must be fully qualified as provider::model"
))
})?;
if provider.is_empty() || model.is_empty() {
return Err(SdkError::Domain(format!(
"model id {s:?} has an empty provider or model half"
)));
}
Ok(Self::new(provider, model))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_and_parses_fully_qualified_ids() {
let id = ModelId::new("anthropic", "claude-opus-5");
assert_eq!(id.to_string(), "anthropic::claude-opus-5");
assert_eq!("anthropic::claude-opus-5".parse::<ModelId>().unwrap(), id);
}
#[test]
fn model_halves_may_contain_separators() {
let id: ModelId = "vertex::publishers/google/models/gemini".parse().unwrap();
assert_eq!(id.provider, "vertex");
assert_eq!(id.model, "publishers/google/models/gemini");
}
#[test]
fn rejects_bare_names_and_empty_halves() {
assert!("claude-opus-5".parse::<ModelId>().is_err());
assert!("::model".parse::<ModelId>().is_err());
assert!("provider::".parse::<ModelId>().is_err());
}
}