#[cfg(feature = "registry")]
use std::path::PathBuf;
use crate::error::{RealizarError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PachaUri {
pub model: String,
pub version: Option<String>,
}
impl PachaUri {
pub fn parse(uri: &str) -> Result<Self> {
let path = uri
.strip_prefix("pacha://")
.ok_or_else(|| RealizarError::InvalidUri(format!("Expected pacha:// scheme: {uri}")))?;
if path.is_empty() {
return Err(RealizarError::InvalidUri(
"Model name required after pacha://".to_string(),
));
}
let (model, version) = if let Some(colon_pos) = path.rfind(':') {
let model = &path[..colon_pos];
let version = &path[colon_pos + 1..];
if model.is_empty() {
return Err(RealizarError::InvalidUri(
"Model name cannot be empty".to_string(),
));
}
if version.is_empty() {
return Err(RealizarError::InvalidUri(
"Version cannot be empty after colon".to_string(),
));
}
(model.to_string(), Some(version.to_string()))
} else {
(path.to_string(), None)
};
Ok(Self { model, version })
}
#[must_use]
pub fn is_pacha_uri(uri: &str) -> bool {
uri.starts_with("pacha://")
}
#[must_use]
pub fn to_uri_string(&self) -> String {
match &self.version {
Some(v) => format!("pacha://{}:{v}", self.model),
None => format!("pacha://{}", self.model),
}
}
#[must_use]
pub fn version_or_latest(&self) -> &str {
self.version.as_deref().unwrap_or("latest")
}
}
impl std::fmt::Display for PachaUri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_uri_string())
}
}
#[derive(Debug, Clone)]
#[cfg(feature = "registry")]
pub struct ModelMetadata {
pub name: String,
pub version: String,
pub content_hash: String,
pub signer_key: Option<String>,
pub local_path: PathBuf,
}
#[cfg(feature = "registry")]
pub struct PachaResolver {
registry: pacha::Registry,
}
#[cfg(feature = "registry")]
impl PachaResolver {
pub fn new() -> Result<Self> {
let registry = pacha::Registry::open_default()
.map_err(|e| RealizarError::RegistryError(format!("Failed to open registry: {e}")))?;
Ok(Self { registry })
}
#[must_use]
pub fn with_registry(registry: pacha::Registry) -> Self {
Self { registry }
}
pub fn resolve(&self, uri: &PachaUri) -> Result<(ModelMetadata, Vec<u8>)> {
use pacha::model::ModelVersion;
let version = match &uri.version {
Some(v) => ModelVersion::parse(v)
.map_err(|e| RealizarError::RegistryError(format!("Invalid version: {e}")))?,
None => ModelVersion::new(0, 0, 0), };
let model = self
.registry
.get_model(&uri.model, &version)
.map_err(|e| RealizarError::ModelNotFound(format!("{}: {e}", uri.model)))?;
let artifact = self
.registry
.get_model_artifact(&uri.model, &version)
.map_err(|e| {
RealizarError::RegistryError(format!("Failed to get model artifact: {e}"))
})?;
let metadata = ModelMetadata {
name: uri.model.clone(),
version: model.version.to_string(),
content_hash: model.content_address.to_string(),
signer_key: None, local_path: PathBuf::new(), };
Ok((metadata, artifact))
}
pub fn get_metadata(&self, uri: &PachaUri) -> Result<ModelMetadata> {
use pacha::model::ModelVersion;
let version = match &uri.version {
Some(v) => ModelVersion::parse(v)
.map_err(|e| RealizarError::RegistryError(format!("Invalid version: {e}")))?,
None => ModelVersion::new(0, 0, 0),
};
let model = self
.registry
.get_model(&uri.model, &version)
.map_err(|e| RealizarError::ModelNotFound(format!("{}: {e}", uri.model)))?;
Ok(ModelMetadata {
name: uri.model.clone(),
version: model.version.to_string(),
content_hash: model.content_address.to_string(),
signer_key: None, local_path: PathBuf::new(),
})
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModelLineage {
pub uri: String,
pub model: String,
pub version: String,
pub content_hash: String,
pub verified: bool,
pub captured_at: u64,
}
impl ModelLineage {
#[cfg(feature = "registry")]
#[must_use]
pub fn from_metadata(uri: &PachaUri, metadata: &ModelMetadata) -> Self {
Self {
uri: uri.to_uri_string(),
model: metadata.name.clone(),
version: metadata.version.clone(),
content_hash: metadata.content_hash.clone(),
verified: metadata.signer_key.is_some(),
captured_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
}
}
#[must_use]
pub fn from_uri(uri: &PachaUri) -> Self {
Self {
uri: uri.to_uri_string(),
model: uri.model.clone(),
version: uri.version.clone().unwrap_or_else(|| "unknown".to_string()),
content_hash: String::new(),
verified: false,
captured_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_full_uri() {
let uri = PachaUri::parse("pacha://llama-7b:1.0.0").unwrap();
assert_eq!(uri.model, "llama-7b");
assert_eq!(uri.version, Some("1.0.0".to_string()));
}
#[test]
fn test_parse_uri_without_version() {
let uri = PachaUri::parse("pacha://mistral-7b").unwrap();
assert_eq!(uri.model, "mistral-7b");
assert_eq!(uri.version, None);
}
#[test]
fn test_parse_uri_with_semver() {
let uri = PachaUri::parse("pacha://gpt2:2.0.0-beta.1").unwrap();
assert_eq!(uri.model, "gpt2");
assert_eq!(uri.version, Some("2.0.0-beta.1".to_string()));
}
#[test]
fn test_parse_uri_with_org() {
let uri = PachaUri::parse("pacha://paiml/trueno-llm:1.0").unwrap();
assert_eq!(uri.model, "paiml/trueno-llm");
assert_eq!(uri.version, Some("1.0".to_string()));
}
#[test]
fn test_invalid_scheme() {
let result = PachaUri::parse("http://example.com/model");
assert!(result.is_err());
}
#[test]
fn test_empty_model() {
let result = PachaUri::parse("pacha://");
assert!(result.is_err());
}
#[test]
fn test_empty_version() {
let result = PachaUri::parse("pacha://model:");
assert!(result.is_err());
}
#[test]
fn test_is_pacha_uri() {
assert!(PachaUri::is_pacha_uri("pacha://model:1.0"));
assert!(PachaUri::is_pacha_uri("pacha://model"));
assert!(!PachaUri::is_pacha_uri("http://example.com"));
assert!(!PachaUri::is_pacha_uri("/path/to/model.gguf"));
}
#[test]
fn test_to_uri_string() {
let uri = PachaUri::parse("pacha://model:1.0").unwrap();
assert_eq!(uri.to_uri_string(), "pacha://model:1.0");
let uri_no_version = PachaUri::parse("pacha://model").unwrap();
assert_eq!(uri_no_version.to_uri_string(), "pacha://model");
}
#[test]
fn test_version_or_latest() {
let uri = PachaUri::parse("pacha://model:2.0").unwrap();
assert_eq!(uri.version_or_latest(), "2.0");
let uri_no_version = PachaUri::parse("pacha://model").unwrap();
assert_eq!(uri_no_version.version_or_latest(), "latest");
}
#[test]
fn test_display() {
let uri = PachaUri::parse("pacha://llama:1.0.0").unwrap();
assert_eq!(format!("{uri}"), "pacha://llama:1.0.0");
}
#[test]
fn test_lineage_from_uri() {
let uri = PachaUri::parse("pacha://model:1.0").unwrap();
let lineage = ModelLineage::from_uri(&uri);
assert_eq!(lineage.uri, "pacha://model:1.0");
assert_eq!(lineage.model, "model");
assert_eq!(lineage.version, "1.0");
assert!(!lineage.verified);
assert!(lineage.captured_at > 0);
}
}