use std::collections::HashMap;
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::client::ConfigProvider;
use crate::config::VerificationStatus;
use crate::error::{ClientError, ConfigError, Result};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BrokenReference {
pub model_id: String,
pub referenced_service: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_path: Option<PathBuf>,
}
impl std::fmt::Display for BrokenReference {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Model '{}' references non-existent service '{}'",
self.model_id, self.referenced_service
)?;
if let Some(path) = &self.config_path {
write!(f, " (in {})", path.display())?;
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct RegistryIndex {
model_to_service: HashMap<String, String>,
service_to_models: HashMap<String, Vec<String>>,
family_to_models: HashMap<String, Vec<String>>,
status_to_models: HashMap<VerificationStatus, Vec<String>>,
service_status_to_services: HashMap<VerificationStatus, Vec<String>>,
service_statuses: HashMap<String, VerificationStatus>,
orphan_services: Vec<String>,
broken_refs: Vec<BrokenReference>,
all_services: Vec<String>,
all_models: Vec<String>,
}
impl RegistryIndex {
pub fn build<P: ConfigProvider>(provider: &P) -> Self {
use std::collections::hash_map::Entry;
let mut index = Self::default();
let services: Vec<String> = provider
.list_services()
.into_iter()
.map(String::from)
.collect();
index.service_to_models.reserve(services.len());
index.service_statuses.reserve(services.len());
for service_name in &services {
index
.service_to_models
.insert(service_name.clone(), Vec::new());
if let Ok(service_config) = provider.get_service(service_name) {
let status = service_config.service.status.clone();
index
.service_statuses
.insert(service_name.clone(), status.clone());
index
.service_status_to_services
.entry(status)
.or_default()
.push(service_name.clone());
}
}
index.all_services = services;
let model_ids: Vec<&str> = provider.list_models();
index.all_models.reserve(model_ids.len());
index.model_to_service.reserve(model_ids.len());
for model_id in model_ids {
let model_id_owned = model_id.to_string();
if let Ok(model_config) = provider.get_model(model_id) {
let service_name = &model_config.model.service;
let family = &model_config.model.family;
let status = &model_config.model.status;
index
.model_to_service
.insert(model_id_owned.clone(), service_name.clone());
match index.service_to_models.entry(service_name.clone()) {
Entry::Occupied(mut entry) => {
entry.get_mut().push(model_id_owned.clone());
}
Entry::Vacant(_) => {
index.broken_refs.push(BrokenReference {
model_id: model_id_owned.clone(),
referenced_service: service_name.clone(),
config_path: None,
});
}
}
index
.family_to_models
.entry(family.clone())
.or_default()
.push(model_id_owned.clone());
index
.status_to_models
.entry(status.clone())
.or_default()
.push(model_id_owned.clone());
}
index.all_models.push(model_id_owned);
}
index.orphan_services = index
.service_to_models
.iter()
.filter_map(|(service, models)| {
if models.is_empty() {
Some(service.clone())
} else {
None
}
})
.collect();
index.orphan_services.sort_unstable();
index.all_models.sort_unstable();
for models in index.service_to_models.values_mut() {
models.sort_unstable();
}
for models in index.family_to_models.values_mut() {
models.sort_unstable();
}
for models in index.status_to_models.values_mut() {
models.sort_unstable();
}
index
}
pub fn has_broken_refs(&self) -> bool {
!self.broken_refs.is_empty()
}
pub fn broken_refs(&self) -> &[BrokenReference] {
&self.broken_refs
}
pub fn orphan_services(&self) -> &[String] {
&self.orphan_services
}
pub fn service_has_models(&self, service: &str) -> bool {
self.service_to_models
.get(service)
.map(|models| !models.is_empty())
.unwrap_or(false)
}
pub fn service_for_model(&self, model_id: &str) -> Option<&str> {
self.model_to_service.get(model_id).map(String::as_str)
}
pub fn models_for_service(&self, service: &str) -> &[String] {
self.service_to_models
.get(service)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn models_in_family(&self, family: &str) -> &[String] {
self.family_to_models
.get(family)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn models_with_status(&self, status: &VerificationStatus) -> &[String] {
self.status_to_models
.get(status)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn service_status(&self, service: &str) -> Option<&VerificationStatus> {
self.service_statuses.get(service)
}
pub fn services_with_status(&self, status: &VerificationStatus) -> &[String] {
self.service_status_to_services
.get(status)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn verified_services(&self) -> &[String] {
self.services_with_status(&VerificationStatus::Verified)
}
pub fn unverified_services(&self) -> &[String] {
self.services_with_status(&VerificationStatus::Unverified)
}
pub fn all_services(&self) -> &[String] {
&self.all_services
}
pub fn all_models(&self) -> &[String] {
&self.all_models
}
pub fn all_families(&self) -> Vec<&str> {
let mut families: Vec<&str> = self.family_to_models.keys().map(String::as_str).collect();
families.sort_unstable();
families
}
pub fn model_counts_by_service(&self) -> HashMap<&str, usize> {
self.service_to_models
.iter()
.map(|(k, v)| (k.as_str(), v.len()))
.collect()
}
pub fn model_counts_by_status(&self) -> HashMap<VerificationStatus, usize> {
self.status_to_models
.iter()
.map(|(k, v)| (k.clone(), v.len()))
.collect()
}
pub fn validate_refs(&self) -> Result<()> {
if self.broken_refs.is_empty() {
Ok(())
} else {
let messages: Vec<String> = self.broken_refs.iter().map(|r| r.to_string()).collect();
Err(ClientError::Config(ConfigError::BrokenReferences(messages)))
}
}
pub fn service_counts_by_status(&self) -> HashMap<VerificationStatus, usize> {
self.service_status_to_services
.iter()
.map(|(k, v)| (k.clone(), v.len()))
.collect()
}
pub fn stats(&self) -> IndexStats {
let model_status_counts = self.model_counts_by_status();
let service_status_counts = self.service_counts_by_status();
IndexStats {
total_services: self.all_services.len(),
verified_services: service_status_counts
.get(&VerificationStatus::Verified)
.copied()
.unwrap_or(0),
unverified_services: service_status_counts
.get(&VerificationStatus::Unverified)
.copied()
.unwrap_or(0),
total_models: self.all_models.len(),
verified_models: model_status_counts
.get(&VerificationStatus::Verified)
.copied()
.unwrap_or(0),
unverified_models: model_status_counts
.get(&VerificationStatus::Unverified)
.copied()
.unwrap_or(0),
total_families: self.family_to_models.len(),
orphan_services: self.orphan_services.len(),
broken_refs: self.broken_refs.len(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IndexStats {
pub total_services: usize,
pub verified_services: usize,
pub unverified_services: usize,
pub total_models: usize,
pub verified_models: usize,
pub unverified_models: usize,
pub total_families: usize,
pub orphan_services: usize,
pub broken_refs: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_index_build() {
use crate::embedded_config::EmbeddedConfigLoader;
let loader = EmbeddedConfigLoader::new().expect("Failed to load configs");
let index = RegistryIndex::build(&loader);
assert!(!index.all_services().is_empty());
assert!(!index.all_models().is_empty());
let anthropic_models = index.models_for_service("anthropic");
assert!(!anthropic_models.is_empty());
assert!(anthropic_models.iter().any(|m| m.contains("claude")));
}
#[test]
fn test_bidirectional_lookup() {
use crate::embedded_config::EmbeddedConfigLoader;
let loader = EmbeddedConfigLoader::new().expect("Failed to load configs");
let index = RegistryIndex::build(&loader);
for model_id in index.all_models().iter().take(10) {
if let Some(service) = index.service_for_model(model_id) {
let models_for_service = index.models_for_service(service);
assert!(
models_for_service.contains(model_id),
"Model {} should be in service {} model list",
model_id,
service
);
}
}
}
}