feat: refresh the model catalog from live API data at runtime

This commit is contained in:
Dark-Alex-17 committed 2026-09-02 14:34:05 -06:00
1 parent 46c8561173
commit b19ac80783
7 files changed
+481 -30

No files matched your search

+137 -1
View File
@@ -8,7 +8,7 @@ use fancy_regex::Regex;
use log::warn;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
pub const QUIRKS_YAML: &str = include_str!("../../quirks.yaml");
@@ -491,6 +491,43 @@ pub fn build_catalog_strict(
Ok(build.providers)
}
/// Replaces or inserts the entry of every degraded/failed provider from the
/// embedded catalog, preserving the fixed provider order of
/// [`PROVIDER_SOURCES`]. A synced override must never carry fewer models for a
/// provider than the embedded catalog: an upstream outage would otherwise
/// shrink the model list until the next successful sync.
pub fn backfill_embedded_floor(build: &mut CatalogBuild, embedded: &[ProviderModels]) {
let needs_backfill: HashSet<&str> = build
.degraded
.iter()
.chain(build.failed.iter())
.map(String::as_str)
.collect();
if needs_backfill.is_empty() {
return;
}
let mut providers = Vec::with_capacity(PROVIDER_SOURCES.len());
for source in &PROVIDER_SOURCES {
let built = build
.providers
.iter()
.position(|provider| provider.provider == source.provider)
.map(|position| build.providers.remove(position));
if needs_backfill.contains(source.provider)
&& let Some(entry) = embedded
.iter()
.find(|provider| provider.provider == source.provider)
{
providers.push(entry.clone());
continue;
}
if let Some(built) = built {
providers.push(built);
}
}
build.providers = providers;
}
const GENERATED_HEADER: &str = "# GENERATED by 'coyote --generate-models' — DO NOT hand-edit model data.\n\
# Hand-owned behavior lives in quirks.yaml; this file is the embedded offline fallback.\n";
@@ -1473,6 +1510,105 @@ rules:
}
}
fn provider_entry(provider: &str, model_names: &[&str]) -> ProviderModels {
ProviderModels {
provider: provider.to_string(),
oauth: None,
models: model_names
.iter()
.map(|name| ModelData::new(name))
.collect(),
}
}
#[test]
fn backfill_replaces_degraded_and_inserts_failed_providers() {
let mut build = CatalogBuild {
providers: vec![
provider_entry("openai", &["gpt-fresh"]),
provider_entry("gemini", &["quirks-only"]),
],
degraded: vec!["gemini".to_string()],
failed: vec!["claude".to_string()],
};
let embedded = vec![
provider_entry("openai", &["gpt-old-1", "gpt-old-2"]),
provider_entry("gemini", &["gem-1", "gem-2", "gem-3"]),
provider_entry("claude", &["claude-1"]),
];
backfill_embedded_floor(&mut build, &embedded);
let order: Vec<&str> = build
.providers
.iter()
.map(|provider| provider.provider.as_str())
.collect();
assert_eq!(order, ["openai", "gemini", "claude"]);
assert_eq!(find_provider(&build, "openai").models.len(), 1);
assert_eq!(find_provider(&build, "gemini").models.len(), 3);
assert_eq!(find_provider(&build, "claude").models.len(), 1);
assert_eq!(
find_provider(&build, "openai").models[0].name,
"gpt-fresh",
"providers with live API data must not be touched"
);
}
#[test]
fn backfill_keeps_built_entry_when_embedded_lacks_the_provider() {
let mut build = CatalogBuild {
providers: vec![provider_entry("gemini", &["quirks-only"])],
degraded: vec!["gemini".to_string()],
failed: vec![],
};
backfill_embedded_floor(&mut build, &[]);
assert_eq!(find_provider(&build, "gemini").models.len(), 1);
}
#[test]
fn backfill_without_degraded_or_failed_is_a_no_op() {
let mut build = CatalogBuild {
providers: vec![provider_entry("openai", &["gpt-fresh"])],
degraded: vec![],
failed: vec![],
};
backfill_embedded_floor(&mut build, &[provider_entry("openai", &["a", "b"])]);
assert_eq!(find_provider(&build, "openai").models.len(), 1);
}
#[test]
fn backfill_restores_embedded_floor_for_unresolved_providers() {
let embedded: Vec<ProviderModels> =
serde_yaml::from_str(crate::client::MODELS_YAML).unwrap();
let mut build = build_catalog(&make_sources(Some(MODELSDEV_FIXTURE), None), &ALL_QUIRKS);
let unresolved: Vec<String> = build
.degraded
.iter()
.chain(build.failed.iter())
.cloned()
.collect();
assert!(!unresolved.is_empty());
backfill_embedded_floor(&mut build, &embedded);
for provider in unresolved {
let built = find_provider(&build, &provider);
let floor = embedded
.iter()
.find(|embedded| embedded.provider == provider)
.unwrap();
assert!(
built.models.len() >= floor.models.len(),
"provider '{provider}' shrank below the embedded floor"
);
}
}
/// Guards against upstream id-shape drift by comparing live API model
/// names against the embedded models.yaml. Run manually:
/// `cargo test idshape_guard_live -- --ignored --nocapture`