use async_trait::async_trait;
use systemprompt_manifest::services::VertexRateCard;
use systemprompt_security::credential::{
AuthHeader, CredentialKind, CredentialScope, ProviderCredential,
};
use systemprompt_wire::hosting::is_vertex_host;
use super::client;
use super::source::{CatalogListing, CatalogSource, DiscoveryEndpoint, DiscoveryError};
#[derive(Debug)]
pub struct VertexCatalog {
card: VertexRateCard,
}
impl VertexCatalog {
#[must_use]
pub const fn new(card: VertexRateCard) -> Self {
Self { card }
}
fn publishers(&self, endpoint: &DiscoveryEndpoint) -> Vec<String> {
self.card.publishers_for(endpoint.wire)
}
}
#[must_use]
pub fn vertex_host(endpoint: &str) -> Option<String> {
let url = url::Url::parse(endpoint).ok()?;
let host = url.host_str()?.to_ascii_lowercase();
if !is_vertex_host(&host) {
return None;
}
Some(format!("{}://{host}", url.scheme()))
}
#[async_trait]
impl CatalogSource for VertexCatalog {
fn name(&self) -> &'static str {
"vertex"
}
fn matches_endpoint(&self, endpoint: &DiscoveryEndpoint) -> bool {
!self.publishers(endpoint).is_empty() && vertex_host(&endpoint.url).is_some()
}
fn applies(&self, endpoint: &DiscoveryEndpoint, credential: &ProviderCredential) -> bool {
self.matches_endpoint(endpoint) && credential.kind() == CredentialKind::GoogleServiceAccount
}
async fn list(
&self,
http: &reqwest::Client,
auth: &AuthHeader,
endpoint: &DiscoveryEndpoint,
_scope: &CredentialScope,
) -> Result<CatalogListing, DiscoveryError> {
let host = vertex_host(&endpoint.url).ok_or_else(|| DiscoveryError::NotVertexHost {
endpoint: endpoint.url.clone(),
})?;
if !auth.is_bearer() {
return Err(DiscoveryError::BearerRequired);
}
let label = endpoint.endpoint.to_string();
let (models, failures) =
client::list_all(http, &host, &auth.value, &label, &self.publishers(endpoint)).await;
Ok(CatalogListing {
models: models
.iter()
.map(super::classify::PublisherModel::discovered)
.collect(),
failures,
})
}
}