use uuid::Uuid;
pub fn parse_model_uri(s: &str) -> Option<String> {
let s = s.strip_prefix("zc://").unwrap_or(s);
let s = s.strip_prefix("model-").unwrap_or(s);
let uuid = Uuid::parse_str(s).ok()?;
Some(uuid.hyphenated().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const UUID: &str = "ae5f3db4-437a-40d1-93ec-c8258315d69a";
#[test]
fn zc_prefixed_uuid() {
assert_eq!(
parse_model_uri(&format!("zc://{UUID}")),
Some(UUID.to_string())
);
}
#[test]
fn zc_model_prefixed_uuid() {
assert_eq!(
parse_model_uri(&format!("zc://model-{UUID}")),
Some(UUID.to_string())
);
}
#[test]
fn bare_uuid() {
assert_eq!(parse_model_uri(UUID), Some(UUID.to_string()));
}
#[test]
fn node_uri_rejected() {
assert_eq!(parse_model_uri("zc://node-abc123"), None);
}
#[test]
fn worker_uri_rejected() {
assert_eq!(parse_model_uri("zc://worker-abc-0"), None);
}
#[test]
fn host_uri_rejected() {
assert_eq!(parse_model_uri("zc://my.zakuro-ai.com:9000"), None);
}
#[test]
fn empty_string_rejected() {
assert_eq!(parse_model_uri(""), None);
}
#[test]
fn uppercase_normalizes_to_lowercase() {
let upper = UUID.to_uppercase();
assert_eq!(
parse_model_uri(&format!("zc://{upper}")),
Some(UUID.to_string())
);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelAddress {
Uuid(String),
Named {
owner: String,
name: String,
digest: Option<String>,
},
}
pub fn parse_model_address(s: &str) -> Option<ModelAddress> {
if let Some(uuid) = parse_model_uri(s) {
return Some(ModelAddress::Uuid(uuid));
}
let rest = s.strip_prefix("zc://")?;
let (owner, tail) = rest.split_once('/')?;
if owner.is_empty() || owner.contains('@') {
return None;
}
let (name, digest) = match tail.split_once("@sha256:") {
Some((n, d)) => {
if d.len() != 64 || !d.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
(n, Some(d.to_ascii_lowercase()))
}
None => (tail, None),
};
if name.is_empty() || name.contains('/') || name.contains('@') {
return None;
}
Some(ModelAddress::Named {
owner: owner.to_string(),
name: name.to_string(),
digest,
})
}
pub fn address_to_ref(addr: &ModelAddress) -> String {
match addr {
ModelAddress::Uuid(u) => format!("zc://{u}"),
ModelAddress::Named {
owner,
name,
digest,
} => match digest {
Some(d) => format!("zc://{owner}/{name}@sha256:{d}"),
None => format!("zc://{owner}/{name}"),
},
}
}
#[cfg(test)]
mod address_tests {
use super::*;
const UUID: &str = "ae5f3db4-437a-40d1-93ec-c8258315d69a";
#[test]
fn uuid_form_needs_no_lookup() {
assert_eq!(
parse_model_address(&format!("zc://{UUID}")),
Some(ModelAddress::Uuid(UUID.to_string()))
);
}
#[test]
fn owner_name_form() {
assert_eq!(
parse_model_address("zc://alice/qwen3-4b"),
Some(ModelAddress::Named {
owner: "alice".into(),
name: "qwen3-4b".into(),
digest: None
})
);
}
#[test]
fn owner_name_pinned() {
let d = "a".repeat(64);
assert_eq!(
parse_model_address(&format!("zc://alice/qwen3-4b@sha256:{d}")),
Some(ModelAddress::Named {
owner: "alice".into(),
name: "qwen3-4b".into(),
digest: Some(d)
})
);
}
#[test]
fn a_short_digest_is_rejected_rather_than_truncated() {
assert_eq!(parse_model_address("zc://alice/m@sha256:abc"), None);
}
#[test]
fn node_and_worker_addresses_are_still_not_models() {
assert_eq!(parse_model_address("zc://node-abc123"), None);
assert_eq!(parse_model_address("zc://worker-abc-0"), None);
}
#[test]
fn empty_segments_are_rejected() {
assert_eq!(parse_model_address("zc:///qwen"), None);
assert_eq!(parse_model_address("zc://alice/"), None);
}
#[test]
fn a_three_segment_path_is_not_an_address() {
assert_eq!(parse_model_address("zc://alice/team/qwen"), None);
}
#[test]
fn round_trips_through_a_ref_string() {
for s in [
&format!("zc://{UUID}"),
"zc://alice/qwen3-4b",
&format!("zc://alice/qwen3-4b@sha256:{}", "b".repeat(64)),
] {
let addr = parse_model_address(s).expect(s);
assert_eq!(address_to_ref(&addr), *s, "round trip for {s}");
}
}
}
pub fn resolve_address(
addr: &ModelAddress,
api_url: &str,
api_key: Option<&str>,
) -> Result<String, String> {
if let ModelAddress::Uuid(u) = addr {
return Ok(u.clone());
}
let reference = address_to_ref(addr);
let url = format!(
"{}/api/models/resolve?ref={}",
api_url.trim_end_matches('/'),
urlencode(&reference)
);
let agent = ureq::Agent::new_with_config(
ureq::Agent::config_builder()
.timeout_connect(Some(std::time::Duration::from_secs(10)))
.timeout_recv_response(Some(std::time::Duration::from_secs(30)))
.http_status_as_error(false)
.build(),
);
let mut req = agent.get(&url);
if let Some(key) = api_key.filter(|k| !k.is_empty()) {
req = req.header("Authorization", &format!("Bearer {key}"));
}
let mut resp = req
.call()
.map_err(|e| format!("could not reach the marketplace at {api_url}: {e}"))?;
let status = resp.status().as_u16();
let body = resp.body_mut().read_to_string().unwrap_or_default();
match status {
200 => {}
404 => {
return Err(format!(
"no such model: {reference}\n \
(a private model resolves only for its owner -- check `zc init`)"
))
}
422 => return Err(format!("not a model address: {reference}")),
s => return Err(format!("marketplace answered {s} for {reference}: {body}")),
}
let v: serde_json::Value =
serde_json::from_str(&body).map_err(|e| format!("unreadable resolve reply: {e}"))?;
v.get("model_id")
.and_then(|m| m.as_str())
.map(|s| s.to_string())
.ok_or_else(|| format!("resolve reply carried no model_id (marketplace too old?): {body}"))
}
fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => out.push(c),
_ => out.push_str(&format!("%{:02X}", c as u32)),
}
}
out
}
#[cfg(test)]
mod resolve_tests {
use super::*;
#[test]
fn a_uuid_address_needs_no_network() {
let uuid = "ae5f3db4-437a-40d1-93ec-c8258315d69a";
assert_eq!(
resolve_address(
&ModelAddress::Uuid(uuid.into()),
"http://127.0.0.1:1/nope",
None
),
Ok(uuid.to_string())
);
}
#[test]
fn encodes_the_reference_for_a_query_string() {
assert_eq!(
urlencode("zc://alice/qwen3-4b"),
"zc%3A%2F%2Falice%2Fqwen3-4b"
);
}
#[test]
fn leaves_unreserved_characters_alone() {
assert_eq!(urlencode("a-z_0.9~"), "a-z_0.9~");
}
}