style: Cleaned up comments and organization of quirks.yaml file

This commit is contained in:
Dark-Alex-17 committed 2026-09-03 12:40:08 -06:00
1 parent 49bd5d86b4
commit dc41e9d5df
3 files changed
+63 -103

No files matched your search

+17 -17
View File
@@ -4,9 +4,9 @@
# model data: oauth blocks, request patches, wire-name aliases, behavioral
# flags, and models that no public catalog provides.
#
# `models:` entries are self-complete — `rules` apply ONLY to API-sourced
# `models:` entries are self-complete. `rules` apply ONLY to API-sourced
# models, never to hand-owned `models:` entries. Legacy verbatim-preserved
# entries are grouped under a `# legacy — pruning candidates` comment.
# entries are grouped under a `# legacy -- pruning candidates` comment.
- provider: openai
exclude:
@@ -78,7 +78,7 @@
max_tokens_per_chunk: 8191
default_chunk_size: 2000
max_batch_size: 100
# legacy — pruning candidates
# legacy -- pruning candidates
- name: o4-mini-high
max_output_tokens: 100000
real_name: o4-mini
@@ -179,7 +179,7 @@
max_tokens_per_chunk: 2048
default_chunk_size: 1500
max_batch_size: 100
# legacy — pruning candidates
# legacy -- pruning candidates
- name: gemini-3-pro-preview
max_input_tokens: 1048576
supports_vision: true
@@ -296,7 +296,7 @@
type: enabled
budget_tokens: 16000
models:
# legacy — pruning candidates
# legacy -- pruning candidates
- name: claude-opus-4-1-20250805
max_input_tokens: 200000
max_output_tokens: 8192
@@ -324,7 +324,7 @@
input_price: 0.1
max_tokens_per_chunk: 8092
default_chunk_size: 2000
# legacy — pruning candidates
# legacy -- pruning candidates
- name: magistral-small-latest
max_input_tokens: 131072
input_price: 0.5
@@ -451,7 +451,7 @@
- provider: perplexity
models:
# legacy — pruning candidates
# legacy -- pruning candidates
- name: sonar-reasoning
max_input_tokens: 128000
input_price: 1
@@ -461,7 +461,7 @@
exclude:
- "whisper-*"
models:
# legacy — pruning candidates
# legacy -- pruning candidates
- name: meta-llama/llama-4-maverick-17b-128e-instruct
max_input_tokens: 131072
input_price: 0
@@ -600,7 +600,7 @@
max_tokens_per_chunk: 2048
default_chunk_size: 1500
max_batch_size: 5
# legacy — pruning candidates
# legacy -- pruning candidates
- name: gemini-3-pro-preview
max_input_tokens: 1048576
supports_vision: true
@@ -813,7 +813,7 @@
max_tokens_per_chunk: 512
default_chunk_size: 1000
max_batch_size: 96
# legacy — pruning candidates
# legacy -- pruning candidates
- name: us.meta.llama3-3-70b-instruct-v1:0
max_input_tokens: 131072
max_output_tokens: 8192
@@ -855,7 +855,7 @@
max_tokens_per_chunk: 512
default_chunk_size: 1000
max_batch_size: 100
# legacy — pruning candidates
# legacy -- pruning candidates
- name: '@cf/google/gemma-3-12b-it'
max_input_tokens: 131072
max_output_tokens: 2048
@@ -939,7 +939,7 @@
max_tokens_per_chunk: 8192
default_chunk_size: 2000
max_batch_size: 10
# legacy — pruning candidates
# legacy -- pruning candidates
- name: qwen3-vl-flash
max_input_tokens: 262144
supports_vision: true
@@ -1000,7 +1000,7 @@
- provider: deepseek
models:
# legacy — pruning candidates
# legacy -- pruning candidates
- name: deepseek-chat
max_input_tokens: 64000
max_output_tokens: 8192
@@ -1040,7 +1040,7 @@
type: reranker
max_input_tokens: 4096
input_price: 0.112
# legacy — pruning candidates
# legacy -- pruning candidates
- name: glm-4.6v-flash
max_input_tokens: 65536
input_price: 0
@@ -1049,7 +1049,7 @@
- provider: minimax
models:
# legacy — pruning candidates
# legacy -- pruning candidates
- name: minimax-m3
max_input_tokens: 1000000
input_price: 4.2
@@ -1166,7 +1166,7 @@
body:
include_reasoning: true
models:
# legacy — pruning candidates
# legacy -- pruning candidates
- name: google/gemini-3-pro-preview
max_input_tokens: 1048576
supports_vision: true
@@ -1352,7 +1352,7 @@
max_tokens_per_chunk: 512
default_chunk_size: 1000
max_batch_size: 100
# legacy — pruning candidates
# legacy -- pruning candidates
- name: Qwen/Qwen3-Max-Thinking
max_input_tokens: 262144
input_price: 1.2
+29 -68
View File
@@ -1,5 +1,6 @@
use super::model::{ModelData, ProviderModels};
use super::oauth::OAuthConfig;
use std::cmp::Ordering;
use crate::utils::fetch;
@@ -9,6 +10,8 @@ use log::warn;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use std::sync::{Arc, LazyLock, Mutex};
pub const QUIRKS_YAML: &str = include_str!("../../quirks.yaml");
@@ -19,10 +22,6 @@ pub static ALL_QUIRKS: LazyLock<Vec<ProviderQuirks>> =
const MODELSDEV_API_URL: &str = "https://models.dev/api.json";
const OPENROUTER_API_URL: &str = "https://openrouter.ai/api/v1/models";
/// Upstream ids for one coyote provider. The table order is the fixed output
/// order of the built catalog. Providers with neither upstream id are fully
/// quirks-owned; `openrouter` is special-cased in the merge: its models come
/// from the OpenRouter list natively instead of via a prefix join.
struct SourceIds {
provider: &'static str,
modelsdev: Option<&'static str>,
@@ -152,15 +151,11 @@ const PROVIDER_SOURCES: [SourceIds; 24] = [
},
];
/// Raw payloads from the two upstream catalogs. Each source is independently
/// fallible; `None` means that fetch failed and the merge degrades gracefully.
pub struct FetchedSources {
pub modelsdev: Option<String>,
pub openrouter: Option<String>,
}
/// Fetches both upstream catalogs concurrently. A failed fetch is logged and
/// surfaces as `None` rather than an error.
pub async fn fetch_sources() -> FetchedSources {
let (modelsdev, openrouter) = tokio::join!(fetch(MODELSDEV_API_URL), fetch(OPENROUTER_API_URL));
FetchedSources {
@@ -173,8 +168,6 @@ pub async fn fetch_sources() -> FetchedSources {
}
}
/// An API-sourced model plus the release date used for ordering. The date is
/// deliberately kept out of [`ModelData`] and never serialized.
struct SourcedModel {
data: ModelData,
release_date: Option<String>,
@@ -192,9 +185,6 @@ fn clamp_price(value: f64) -> Option<f64> {
(0.0..=10_000.0).contains(&value).then_some(value)
}
/// Extracts the models of one models.dev provider. Tolerant by design:
/// missing or malformed fields are skipped while the model is kept, and
/// out-of-range numbers are dropped field-wise.
fn modelsdev_provider_models(api: &Value, provider_id: &str) -> Vec<SourcedModel> {
let Some(models) = api[provider_id]["models"].as_object() else {
return Vec::new();
@@ -236,15 +226,11 @@ fn modelsdev_provider_models(api: &Value, provider_id: &str) -> Vec<SourcedModel
.collect()
}
/// OpenRouter prices are USD per token; the catalog stores USD per million
/// tokens, rounded to 4 decimals.
fn openrouter_price(value: &Value) -> Option<f64> {
let per_token: f64 = value.as_str()?.parse().ok()?;
clamp_price((per_token * 1_000_000.0 * 10_000.0).round() / 10_000.0)
}
/// Maps one OpenRouter list entry onto [`ModelData`], with the same field-wise
/// tolerance and clamping as the models.dev adapter.
fn openrouter_model_data(name: &str, entry: &Value) -> ModelData {
let mut data = ModelData::new(name);
data.max_input_tokens = entry["context_length"]
@@ -276,9 +262,6 @@ fn openrouter_index(payload: &Value) -> HashMap<&str, &Value> {
.unwrap_or_default()
}
/// Every OpenRouter list entry as a model of the `openrouter` provider, named
/// by its full id. OpenRouter publishes no release date, so ordering falls
/// back to name-ascending.
fn openrouter_native_models(payload: &Value) -> Vec<SourcedModel> {
payload["data"]
.as_array()
@@ -297,8 +280,6 @@ fn openrouter_native_models(payload: &Value) -> Vec<SourcedModel> {
.unwrap_or_default()
}
/// Fills fields that models.dev left unset on the same model: OpenRouter data
/// never overrides a present models.dev value.
fn fill_from_openrouter(data: &mut ModelData, entry: &Value) {
let filler = openrouter_model_data(&data.name, entry);
if data.max_input_tokens.is_none() {
@@ -427,8 +408,8 @@ pub fn build_catalog(sources: &FetchedSources, quirks: &[ProviderQuirks]) -> Cat
(Some(a_date), Some(b_date)) => b_date
.cmp(a_date)
.then_with(|| a.data.name.cmp(&b.data.name)),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => a.data.name.cmp(&b.data.name),
});
@@ -474,8 +455,6 @@ pub fn build_catalog(sources: &FetchedSources, quirks: &[ProviderQuirks]) -> Cat
}
}
/// Like [`build_catalog`], but errors unless every provider was assembled
/// from live API data (or is quirks-owned by design).
pub fn build_catalog_strict(
sources: &FetchedSources,
quirks: &[ProviderQuirks],
@@ -488,6 +467,7 @@ pub fn build_catalog_strict(
build.failed.join(", ")
);
}
Ok(build.providers)
}
@@ -528,13 +508,10 @@ pub fn backfill_embedded_floor(build: &mut CatalogBuild, embedded: &[ProviderMod
build.providers = providers;
}
const GENERATED_HEADER: &str = "# GENERATED by 'coyote --generate-models' — DO NOT hand-edit model data.\n\
const GENERATED_HEADER: &str = "# GENERATED by 'coyote --generate-models' \n#DO NOT hand-edit model data.\n\
# Hand-owned behavior lives in quirks.yaml; this file is the embedded offline fallback.\n";
/// Builds the catalog from live API data and writes it to `path`. Both
/// upstream sources must respond and every provider must fully resolve;
/// identical source data always produces identical bytes.
pub async fn generate_models_file(path: &std::path::Path) -> Result<()> {
pub async fn generate_models_file(path: &Path) -> Result<()> {
let sources = fetch_sources().await;
if sources.modelsdev.is_none() || sources.openrouter.is_none() {
bail!(
@@ -544,25 +521,24 @@ pub async fn generate_models_file(path: &std::path::Path) -> Result<()> {
let providers = build_catalog_strict(&sources, &ALL_QUIRKS)?;
let model_count: usize = providers.iter().map(|provider| provider.models.len()).sum();
let yaml = serde_yaml::to_string(&providers)?;
std::fs::write(path, format!("{GENERATED_HEADER}{yaml}"))?;
fs::write(path, format!("{GENERATED_HEADER}{yaml}"))?;
println!(
"Wrote {} models across {} providers to {}",
model_count,
providers.len(),
path.display()
);
Ok(())
}
/// Fraction of `old` names that also appear in `new`; empty `old` yields 1.0.
/// Used to guard against upstream id-shape drift: a low overlap means the
/// upstream catalog renamed its models out from under us.
#[allow(dead_code)]
#[cfg(test)]
pub(crate) fn name_overlap(old: &[String], new: &[String]) -> f64 {
if old.is_empty() {
return 1.0;
}
let new_names: std::collections::HashSet<&str> = new.iter().map(String::as_str).collect();
let new_names: HashSet<&str> = new.iter().map(String::as_str).collect();
let kept = old
.iter()
.filter(|name| new_names.contains(name.as_str()))
@@ -570,10 +546,6 @@ pub(crate) fn name_overlap(old: &[String], new: &[String]) -> f64 {
kept as f64 / old.len() as f64
}
/// Overlays a hand-owned quirks entry onto the API-sourced model of the same
/// name. Every field the hand-owned entry carries (Some, true, or non-empty)
/// wins; absent fields keep the API value. `model_type` is authoritative on
/// the hand-owned entry.
fn overlay_hand_owned(hand: ModelData, api: ModelData) -> ModelData {
ModelData {
name: hand.name,
@@ -604,9 +576,6 @@ fn overlay_hand_owned(hand: ModelData, api: ModelData) -> ModelData {
}
}
/// Hand-owned catalog overrides for a single provider: oauth configuration,
/// curation excludes, field overlays for API-sourced models, suffixed model
/// variants, and fully hand-owned model entries.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProviderQuirks {
@@ -623,10 +592,6 @@ pub struct ProviderQuirks {
pub models: Vec<ModelData>,
}
/// A field overlay applied to every API-sourced model whose name matches the
/// glob in `match` (`*` = any run of characters, `?` = exactly one). Only
/// explicitly present fields are written; absent fields leave the model
/// untouched.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct QuirkRule {
@@ -661,9 +626,6 @@ pub struct QuirkRule {
pub system_prompt_prefix: Option<String>,
}
/// Clones every model whose name matches the glob in `match`, appends `suffix`
/// to the clone's name, and overlays any explicitly present fields onto the
/// clone.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct QuirkVariant {
@@ -699,8 +661,6 @@ pub struct QuirkVariant {
pub system_prompt_prefix: Option<String>,
}
/// Applies every rule whose glob matches `data.name`, in file order; later
/// rules overwrite earlier ones on the same field.
pub fn apply_rules(provider_quirks: &ProviderQuirks, data: &mut ModelData) {
for rule in &provider_quirks.rules {
if !glob_matches(&rule.r#match, &data.name) {
@@ -751,8 +711,6 @@ pub fn apply_rules(provider_quirks: &ProviderQuirks, data: &mut ModelData) {
}
}
/// Overlays a variant's explicitly present fields onto a cloned model; same
/// field semantics as [`apply_rules`].
fn apply_variant(variant: &QuirkVariant, data: &mut ModelData) {
if let Some(real_name) = &variant.real_name {
data.real_name = Some(real_name.clone());
@@ -798,9 +756,6 @@ fn apply_variant(variant: &QuirkVariant, data: &mut ModelData) {
}
}
/// Finds the quirks for a client: an exact provider match wins, otherwise a
/// client whose name starts with the provider name (openai-compatible clients
/// are conventionally named after the provider they wrap).
pub fn quirks_for<'a>(
quirks: &'a [ProviderQuirks],
client_name: &str,
@@ -815,9 +770,6 @@ pub fn quirks_for<'a>(
})
}
/// Compiled patterns are memoized: catalog builds evaluate every rule, exclude,
/// and variant glob against every model, so recompiling per call would dominate
/// the merge as the catalog grows.
static GLOB_CACHE: LazyLock<Mutex<HashMap<String, Arc<Regex>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
@@ -834,8 +786,6 @@ fn glob_matches(pattern: &str, name: &str) -> bool {
regex.is_match(name).unwrap_or(false)
}
/// Translates a glob pattern (`*` = any run of characters, `?` = exactly one)
/// into an anchored regex. Patterns that fail to compile match nothing.
fn compile_glob(pattern: &str) -> Regex {
let translated = format!(
"^{}$",
@@ -853,6 +803,7 @@ fn compile_glob(pattern: &str) -> Regex {
mod tests {
use super::*;
use crate::client::MODELS_YAML;
use serde_json::json;
const KNOWN_PROVIDERS: [&str; 24] = [
@@ -934,6 +885,7 @@ mod tests {
.iter()
.map(|source| source.provider)
.collect();
assert_eq!(order, KNOWN_PROVIDERS);
}
@@ -1122,7 +1074,9 @@ rules:
.iter()
.flat_map(|id| modelsdev_provider_models(&api, id))
.collect();
all.extend(openrouter_native_models(&payload));
assert!(!all.is_empty());
for model in &all {
let data = &model.data;
@@ -1154,7 +1108,9 @@ rules:
token_url: https://example.com/token
"#,
);
let build = build_catalog(&fixture_sources(), &quirks);
assert!(find_provider(&build, "xai").oauth.is_some());
assert!(find_provider(&build, "openai").oauth.is_none());
}
@@ -1352,7 +1308,9 @@ rules:
models: [{name: jamba}]
"#,
);
let build = build_catalog(&make_sources(None, None), &quirks);
assert_eq!(build.degraded, ["gemini"]);
assert!(build.failed.contains(&"openai".to_string()));
assert!(build.failed.contains(&"openrouter".to_string()));
@@ -1383,9 +1341,11 @@ rules:
models: [{name: g-1}]
"#,
);
let err = build_catalog_strict(&make_sources(None, None), &quirks)
.unwrap_err()
.to_string();
assert!(err.contains("gemini"), "{err}");
assert!(err.contains("openai"), "{err}");
}
@@ -1416,6 +1376,7 @@ rules:
models: [{name: v}]
"#,
);
let providers =
build_catalog_strict(&make_sources(Some(&modelsdev), Some(&openrouter)), &quirks)
.unwrap();
@@ -1423,6 +1384,7 @@ rules:
.iter()
.map(|provider| provider.provider.as_str())
.collect();
assert_eq!(order, KNOWN_PROVIDERS);
}
@@ -1430,13 +1392,14 @@ rules:
fn name_overlap_fraction_of_old_names_still_present() {
let old: Vec<String> = ["a", "b", "c", "d"].map(str::to_string).into();
let new: Vec<String> = ["a", "c", "x"].map(str::to_string).into();
assert_eq!(name_overlap(&old, &new), 0.5);
assert_eq!(name_overlap(&[], &new), 1.0);
assert_eq!(name_overlap(&old, &[]), 0.0);
}
fn embedded_catalog() -> Vec<ProviderModels> {
serde_yaml::from_str(crate::client::MODELS_YAML).unwrap()
serde_yaml::from_str(MODELS_YAML).unwrap()
}
fn embedded_provider<'a>(providers: &'a [ProviderModels], name: &str) -> &'a ProviderModels {
@@ -1598,8 +1561,7 @@ rules:
#[test]
fn backfill_restores_embedded_floor_for_unresolved_providers() {
let embedded: Vec<ProviderModels> =
serde_yaml::from_str(crate::client::MODELS_YAML).unwrap();
let embedded: Vec<ProviderModels> = serde_yaml::from_str(MODELS_YAML).unwrap();
let mut build = build_catalog(&make_sources(Some(MODELSDEV_FIXTURE), None), &ALL_QUIRKS);
let unresolved: Vec<String> = build
.degraded
@@ -1645,8 +1607,7 @@ rules:
.expect("openrouter fetch failed"),
)
.unwrap();
let embedded: Vec<ProviderModels> =
serde_yaml::from_str(crate::client::MODELS_YAML).unwrap();
let embedded: Vec<ProviderModels> = serde_yaml::from_str(MODELS_YAML).unwrap();
println!(
"{:<12} {:>7} {:>5} {:>5}",
+17 -18
View File
@@ -94,7 +94,7 @@ use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use std::{
env,
env, fs,
fs::{File, create_dir_all, read_dir, read_to_string},
io::Write,
path::{Path, PathBuf},
@@ -548,8 +548,6 @@ pub async fn sync_models(url: Option<&str>, abort_signal: AbortSignal) -> Result
Ok(())
}
/// [`sync_models`] without a spinner or terminal output, for background use;
/// failures are returned for the caller to log.
pub async fn sync_models_quiet(url: Option<&str>) -> Result<()> {
let list = match url {
Some(url) => {
@@ -589,6 +587,7 @@ fn build_refreshed_catalog(
if sources.openrouter.is_none() {
warnings.push("Failed to fetch OpenRouter; continuing with models.dev data only".into());
}
let mut build = catalog::build_catalog(sources, &catalog::ALL_QUIRKS);
if !build.degraded.is_empty() {
warnings.push(format!(
@@ -602,12 +601,15 @@ fn build_refreshed_catalog(
build.failed.join(", ")
));
}
let embedded = serde_yaml::from_str::<Vec<ProviderModels>>(MODELS_YAML)
.with_context(|| "Failed to parse the embedded models.yaml")?;
catalog::backfill_embedded_floor(&mut build, &embedded);
if build.providers.is_empty() {
bail!("Model sync produced no providers");
}
Ok((build.providers, warnings))
}
@@ -626,20 +628,18 @@ fn write_models_override(list: Vec<ProviderModels>) -> Result<PathBuf> {
// name embeds the pid so concurrent coyote processes (auto-refresh racing
// a manual --sync-models) never interleave writes into the same file.
let mut tmp_name = model_override_path.as_os_str().to_os_string();
tmp_name.push(format!(".{}.tmp", std::process::id()));
tmp_name.push(format!(".{}.tmp", process::id()));
let tmp_path = PathBuf::from(tmp_name);
std::fs::write(&tmp_path, models_override_data)
fs::write(&tmp_path, models_override_data)
.with_context(|| format!("Failed to write to '{}'", tmp_path.display()))?;
if let Err(error) = std::fs::rename(&tmp_path, &model_override_path) {
let _ = std::fs::remove_file(&tmp_path);
if let Err(error) = fs::rename(&tmp_path, &model_override_path) {
let _ = fs::remove_file(&tmp_path);
return Err(error)
.with_context(|| format!("Failed to write to '{}'", model_override_path.display()));
}
Ok(model_override_path)
}
/// A missing, unreadable, corrupt, or version-mismatched override is always
/// stale; a loadable one is stale once its mtime is older than the TTL.
fn models_override_is_stale(loadable: bool, mtime: Option<SystemTime>, ttl_hours: u64) -> bool {
if !loadable {
return true;
@@ -653,15 +653,12 @@ fn models_override_is_stale(loadable: bool, mtime: Option<SystemTime>, ttl_hours
}
}
/// Fire-and-forget refresh of the models override on REPL startup. The
/// running session keeps its already-loaded catalog; the next start picks up
/// the refreshed file.
pub fn maybe_spawn_models_refresh(app: &AppConfig) {
if !app.sync_models_auto || app.dry_run {
return;
}
let loadable = paths::local_models_override().is_ok();
let mtime = std::fs::metadata(paths::models_override_file())
let mtime = fs::metadata(paths::models_override_file())
.and_then(|metadata| metadata.modified())
.ok();
if !models_override_is_stale(loadable, mtime, app.sync_models_ttl_hours) {
@@ -1030,13 +1027,13 @@ fn render_config_template_from(
fn write_config_file(config_path: &Path, config_data: &str) -> Result<()> {
ensure_parent_exists(config_path)?;
std::fs::write(config_path, config_data)
fs::write(config_path, config_data)
.with_context(|| format!("Failed to write to '{}'", config_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::prelude::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(config_path, perms)?;
let perms = fs::Permissions::from_mode(0o600);
fs::set_permissions(config_path, perms)?;
}
println!("✓ Saved the config file to '{}'.\n", config_path.display());
@@ -1545,6 +1542,7 @@ clients:
let margin = std::time::Duration::from_secs(300);
let just_fresh = SystemTime::now() - (ttl - margin);
let just_stale = SystemTime::now() - (ttl + margin);
assert!(!models_override_is_stale(true, Some(just_fresh), ttl_hours));
assert!(models_override_is_stale(true, Some(just_stale), ttl_hours));
}
@@ -1552,6 +1550,7 @@ clients:
#[test]
fn future_mtime_is_not_stale() {
let future = SystemTime::now() + std::time::Duration::from_secs(3600);
assert!(!models_override_is_stale(true, Some(future), 24));
}
@@ -1564,8 +1563,8 @@ clients:
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let tmp_dir = std::env::temp_dir().join(format!("coyote-models-override-{unique}"));
fs::create_dir_all(&tmp_dir).unwrap();
let tmp_dir = env::temp_dir().join(format!("coyote-models-override-{unique}"));
create_dir_all(&tmp_dir).unwrap();
let env_name = get_env_name("config_dir");
let prev = env::var_os(&env_name);