use bitrouter_ai::types::ProtocolList;
use bitrouter_sdk::config::{
Config, Pattern, PatternMap, PricingConfig, PricingTierConfig, ProviderClass, ProviderConfig,
ProviderModel, RateLimit, env_lookup, substitute_with,
};
use bitrouter_ai::catalog::types::{
Billing, RegistryData, RegistryKind, RegistryPricing, RegistryProvider, RegistryRateLimits,
};
const BITROUTER_CLOUD_ID: &str = "bitrouter";
pub fn apply_registry(config: &mut Config, data: &RegistryData) {
if !config.inherit_defaults || !config.registry.enabled {
return;
}
for provider in &data.providers {
if !provider.is_active() || !provider.is_public() {
continue;
}
merge_provider(config, provider);
}
for (provider_id, provider) in &mut config.providers {
for model in &mut provider.models {
let canonical_id =
data.providers
.iter()
.find(|entry| entry.name == *provider_id)
.and_then(|entry| {
entry.models.iter().find(|entry| {
entry.id == model.id || entry.provider_model_id == model.id
})
})
.map_or(model.id.as_str(), |entry| entry.id.as_str());
if let Some(metadata) = data.canonical.iter().find(|entry| entry.id == canonical_id) {
let limits = &mut model.token_limits;
limits.max_input_tokens = limits.max_input_tokens.or(metadata.max_input_tokens);
limits.max_output_tokens = limits.max_output_tokens.or(metadata.max_output_tokens);
limits.context_window = limits.context_window.or(metadata.context_window);
}
}
}
}
fn resolve_api_base(raw: &str) -> Option<String> {
substitute_with(raw, env_lookup).ok()
}
fn merge_provider(config: &mut Config, provider: &RegistryProvider) {
let id = provider.name.as_str();
let class = classify(provider);
let protocol_map = provider_protocol_map(provider);
if let Some(existing) = config.providers.get_mut(id) {
let subscription = matches!(
class,
ProviderClass::FirstPartySubscription | ProviderClass::GatewaySubscription
);
let hand_declared = !existing.models.is_empty()
&& (!existing.api_key.is_empty() || !existing.accounts.is_empty());
if existing.class.is_none() && !(subscription && hand_declared) {
existing.class = Some(class);
}
if existing.models.is_empty() {
existing.models = build_models(provider);
}
if existing.api_base.is_empty()
&& let Some(api_base) = &provider.api_base
{
match resolve_api_base(api_base) {
Some(base) => existing.api_base = base,
None => existing.active = false,
}
}
if existing.api_protocol.is_empty()
&& let Some(map) = &protocol_map
{
existing.api_protocol = map.clone();
}
if existing.protocol_endpoints.is_empty() {
existing.protocol_endpoints = protocol_endpoints(provider);
}
if existing.accounts.is_empty()
&& existing.api_key.is_empty()
&& let Some(var) = provider.env_credential_var()
{
match env_lookup(&var).filter(|v| !v.is_empty()) {
Some(key) => existing.api_key = key,
None if id != BITROUTER_CLOUD_ID => existing.active = false,
None => {}
}
}
return;
}
let Some(var) = provider.env_credential_var() else {
return;
};
let Some(api_key) = env_lookup(&var).filter(|v| !v.is_empty()) else {
return;
};
let Some(api_base) = provider.api_base.as_deref().and_then(resolve_api_base) else {
return;
};
let models = build_models(provider);
let entry = ProviderConfig {
api_key,
api_base,
api_protocol: protocol_map.unwrap_or_default(),
protocol_endpoints: protocol_endpoints(provider),
models,
class: Some(class),
active: true,
..ProviderConfig::default()
};
config.providers.insert(id.to_string(), entry);
}
fn classify(provider: &RegistryProvider) -> ProviderClass {
match provider.kind.unwrap_or(if provider.community {
RegistryKind::ThirdParty
} else {
RegistryKind::FirstParty
}) {
RegistryKind::Cloud => ProviderClass::BitrouterCloud,
RegistryKind::Gateway => ProviderClass::GatewaySubscription,
RegistryKind::ThirdParty => ProviderClass::ThirdPartyApi,
RegistryKind::FirstParty => {
if provider.billing == Billing::Subscription {
ProviderClass::FirstPartySubscription
} else {
ProviderClass::FirstPartyApi
}
}
}
}
fn provider_protocol_map(provider: &RegistryProvider) -> Option<PatternMap<ProtocolList>> {
if provider.api_protocol.is_empty() {
return None;
}
let mut map = PatternMap::new();
for entry in &provider.api_protocol {
for (pattern, set) in entry {
map.push(Pattern::parse(pattern), set.to_protocol_list());
}
}
Some(map)
}
fn protocol_endpoints(provider: &RegistryProvider) -> std::collections::HashMap<String, String> {
provider
.protocol_endpoints
.as_ref()
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
fn build_models(provider: &RegistryProvider) -> Vec<ProviderModel> {
provider
.models
.iter()
.map(|m| ProviderModel {
id: m.id.clone(),
provider_model_id: Some(m.provider_model_id.clone()),
api_protocol: Some(m.api_protocol.to_protocol_list()),
rate_limits: m.rate_limits.as_ref().map(map_rate_limits),
pricing: m.pricing.as_ref().and_then(map_pricing),
pricing_by_protocol: m
.pricing_by_protocol
.iter()
.map(|(protocol, pricing)| {
(protocol.clone(), map_pricing(pricing).unwrap_or_default())
})
.collect(),
capabilities: m.capabilities.clone(),
token_limits: Default::default(),
input_token_counting: None,
reasoning_effort: m.reasoning_effort.clone(),
compatibility: m.compatibility.clone(),
})
.collect()
}
fn map_rate_limits(rl: &RegistryRateLimits) -> RateLimit {
RateLimit {
requests_per_minute: rl.requests_per_minute,
tokens_per_minute: rl.tokens_per_minute,
}
}
fn map_pricing(p: &RegistryPricing) -> Option<PricingConfig> {
let input = p.input_tokens.as_ref().and_then(|i| i.no_cache);
let cache_read = p.input_tokens.as_ref().and_then(|i| i.cache_read);
let cache_write = p.input_tokens.as_ref().and_then(|i| i.cache_write);
let output = p.output_tokens.as_ref().and_then(|o| o.text);
let context_tiers: Vec<PricingTierConfig> = p
.context_tiers
.iter()
.map(|t| PricingTierConfig {
above_input_tokens: t.above_input_tokens,
input_micro_usd_per_token: t.input_tokens.as_ref().and_then(|i| i.no_cache),
cache_read_micro_usd_per_token: t.input_tokens.as_ref().and_then(|i| i.cache_read),
cache_write_micro_usd_per_token: t.input_tokens.as_ref().and_then(|i| i.cache_write),
output_micro_usd_per_token: t.output_tokens.as_ref().and_then(|o| o.text),
})
.collect();
if input.is_none()
&& cache_read.is_none()
&& cache_write.is_none()
&& output.is_none()
&& context_tiers.is_empty()
&& p.endpoint_profile.is_none()
{
return None;
}
Some(PricingConfig {
endpoint_profile: p.endpoint_profile,
input_micro_usd_per_token: input,
cache_read_micro_usd_per_token: cache_read,
cache_write_micro_usd_per_token: cache_write,
output_micro_usd_per_token: output,
context_tiers,
})
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
use bitrouter_ai::catalog::types::{
CanonicalModel, InputTokenPricing, OutputTokenPricing, ProtocolSet, RegistryAccess,
RegistryAuth, RegistryAuthKind, RegistryModel, RegistryProtocol, RequiredConfig,
};
use bitrouter_ai::types::{ApiProtocol, ChatTokenLimitField, ProtocolList};
fn provider(name: &str) -> RegistryProvider {
RegistryProvider {
name: name.to_string(),
display_name: None,
api_base: Some(format!("https://{name}.example/v1")),
api_protocol: Vec::new(),
protocol_endpoints: None,
models: vec![RegistryModel {
id: "deepseek/deepseek-v3.2".to_string(),
provider_model_id: "deepseek-v3.2".to_string(),
api_protocol: ProtocolSet::One(RegistryProtocol::Openai),
pricing_by_protocol: std::collections::HashMap::new(),
pricing: Some(RegistryPricing {
endpoint_profile: None,
input_tokens: Some(InputTokenPricing {
no_cache: Some(0.27),
cache_read: None,
cache_write: None,
}),
output_tokens: Some(OutputTokenPricing { text: Some(0.41) }),
context_tiers: Vec::new(),
}),
rate_limits: None,
capabilities: Vec::new(),
reasoning_effort: None,
compatibility: Default::default(),
}],
status: "active".to_string(),
kind: None,
auth: None,
required_config: Vec::new(),
doc_url: None,
community: false,
access: Some(RegistryAccess::ApiKey),
byok: Some(true),
billing: Billing::UsageToken,
}
}
#[test]
fn canonical_capacities_fill_aliases_without_overwriting_provider_limits() -> anyhow::Result<()>
{
let entry = provider("capacity-fixture");
let mut config = Config::default();
let mut models = build_models(&entry);
models[0].id = entry.models[0].provider_model_id.clone();
models[0].token_limits.max_output_tokens = Some(42);
config.providers.insert(
entry.name.clone(),
ProviderConfig {
models,
..Default::default()
},
);
let data = RegistryData {
canonical: vec![CanonicalModel {
id: entry.models[0].id.clone(),
max_input_tokens: Some(1000),
max_output_tokens: Some(500),
context_window: None,
}],
providers: vec![entry],
};
apply_registry(&mut config, &data);
let model = &config
.providers
.get("capacity-fixture")
.ok_or_else(|| anyhow::anyhow!("missing provider"))?
.models[0];
assert_eq!(model.token_limits.max_input_tokens, Some(1000));
assert_eq!(model.token_limits.max_output_tokens, Some(42));
assert_eq!(model.token_limits.context_window, None);
Ok(())
}
#[test]
fn model_capabilities_and_compatibility_reach_provider_model() -> anyhow::Result<()> {
let mut registry_provider = provider("regtestprov");
registry_provider.models[0].capabilities = vec![
bitrouter_ai::types::Capability::Reasoning,
bitrouter_ai::types::Capability::Tools,
];
registry_provider.models[0].reasoning_effort =
Some(bitrouter_ai::types::ReasoningEffortConfig {
levels: vec![
bitrouter_ai::types::ReasoningEffort::Low,
bitrouter_ai::types::ReasoningEffort::High,
],
default: Some(bitrouter_ai::types::ReasoningEffort::High),
});
registry_provider.models[0]
.compatibility
.chat_completions
.token_limit_field = Some(ChatTokenLimitField::MaxCompletionTokens);
let models = build_models(®istry_provider);
assert_eq!(
models[0].compatibility.chat_completions.token_limit_field,
Some(ChatTokenLimitField::MaxCompletionTokens)
);
assert_eq!(
models[0].capabilities,
[
bitrouter_ai::types::Capability::Reasoning,
bitrouter_ai::types::Capability::Tools,
]
);
assert_eq!(
models[0]
.reasoning_effort
.as_ref()
.and_then(|config| config.default),
Some(bitrouter_ai::types::ReasoningEffort::High)
);
Ok(())
}
#[test]
fn registry_cache_rates_map_without_fabricating_missing_values() -> anyhow::Result<()> {
let pricing = RegistryPricing {
endpoint_profile: None,
input_tokens: Some(InputTokenPricing {
no_cache: Some(3.0),
cache_read: Some(0.3),
cache_write: Some(3.75),
}),
output_tokens: Some(OutputTokenPricing { text: None }),
context_tiers: Vec::new(),
};
let mapped = map_pricing(&pricing).context("partial pricing retained")?;
assert_eq!(mapped.input_micro_usd_per_token, Some(3.0));
assert_eq!(mapped.cache_read_micro_usd_per_token, Some(0.3));
assert_eq!(mapped.cache_write_micro_usd_per_token, Some(3.75));
assert_eq!(mapped.output_micro_usd_per_token, None);
Ok(())
}
fn data_with(providers: Vec<RegistryProvider>, canonical: Vec<&str>) -> RegistryData {
RegistryData {
providers,
canonical: canonical
.into_iter()
.map(|id| CanonicalModel {
id: id.to_string(),
..Default::default()
})
.collect(),
}
}
use crate::providers::test_env::with_env;
#[test]
fn classifies_by_community_and_billing() -> anyhow::Result<()> {
let mut first_party = provider("zai");
first_party.community = false;
first_party.billing = Billing::UsageToken;
assert_eq!(classify(&first_party), ProviderClass::FirstPartyApi);
let mut sub = provider("zai_coding_plan");
sub.billing = Billing::Subscription;
assert_eq!(classify(&sub), ProviderClass::FirstPartySubscription);
let mut reseller = provider("chutes");
reseller.community = true;
reseller.billing = Billing::Subscription;
assert_eq!(classify(&reseller), ProviderClass::ThirdPartyApi);
Ok(())
}
#[test]
fn hand_declared_subscription_keeps_class_unset() -> anyhow::Result<()> {
let mut sub = provider("mysub");
sub.billing = Billing::Subscription;
let hand_models = build_models(&sub);
assert!(!hand_models.is_empty());
let mut config = Config::default();
config.providers.insert(
"mysub".to_string(),
ProviderConfig {
api_key: "sub-oauth-placeholder".to_string(),
active: true,
models: hand_models,
..ProviderConfig::default()
},
);
apply_registry(&mut config, &data_with(vec![sub], vec![]));
let merged = &config.providers["mysub"];
assert_eq!(
merged.class, None,
"a fully hand-declared subscription provider opts into the cascade"
);
assert!(!merged.models.is_empty(), "its own models are preserved");
Ok(())
}
#[test]
fn stub_subscription_still_classified() -> anyhow::Result<()> {
let mut sub = provider("mysub2");
sub.billing = Billing::Subscription;
let mut config = Config::default();
config.providers.insert(
"mysub2".to_string(),
ProviderConfig {
active: true,
..ProviderConfig::default()
},
);
apply_registry(&mut config, &data_with(vec![sub], vec![]));
assert_eq!(
config.providers["mysub2"].class,
Some(ProviderClass::FirstPartySubscription),
"a stub subscription provider is still classified"
);
Ok(())
}
#[test]
fn credential_present_inserts_active_provider_with_models() -> anyhow::Result<()> {
with_env("REGTESTPROV_API_KEY", Some("sk-test"), || {
let mut config = Config::default();
let p = provider("regtestprov");
apply_registry(
&mut config,
&data_with(vec![p], vec!["deepseek/deepseek-v3.2"]),
);
let merged = config
.providers
.get("regtestprov")
.context("credentialed provider must be inserted")?;
assert!(merged.active);
assert_eq!(merged.api_key, "sk-test");
assert_eq!(merged.api_base, "https://regtestprov.example/v1");
assert_eq!(merged.class, Some(ProviderClass::FirstPartyApi));
assert_eq!(merged.models.len(), 1);
let model = &merged.models[0];
assert_eq!(model.id, "deepseek/deepseek-v3.2");
assert_eq!(model.provider_model_id.as_deref(), Some("deepseek-v3.2"));
assert_eq!(
model.api_protocol,
Some(ProtocolList(vec![ApiProtocol::ChatCompletions]))
);
let pricing = model.pricing.as_ref().context("pricing mapped")?;
assert_eq!(pricing.input_micro_usd_per_token, Some(0.27));
assert_eq!(pricing.output_micro_usd_per_token, Some(0.41));
Ok(())
})?;
Ok(())
}
#[test]
fn merged_provider_carries_per_model_protocol() -> anyhow::Result<()> {
let mut config = Config::default();
config.providers.insert(
"openai".to_string(),
ProviderConfig {
active: true,
..ProviderConfig::default()
},
);
apply_registry(&mut config, &data_with(vec![provider("openai")], vec![]));
let openai = &config.providers["openai"];
assert!(!openai.models.is_empty(), "registry catalog is merged in");
assert_eq!(
openai.models[0].api_protocol,
Some(ProtocolList(vec![ApiProtocol::ChatCompletions])),
"the merge pins each model's dist-resolved protocol"
);
Ok(())
}
#[test]
fn credential_absent_skips_provider() -> anyhow::Result<()> {
with_env("REGABSENTPROV_API_KEY", None, || {
let mut config = Config::default();
let p = provider("regabsentprov");
apply_registry(&mut config, &data_with(vec![p], vec![]));
assert!(
!config.providers.contains_key("regabsentprov"),
"no credential ⇒ provider must not be activated"
);
Ok(())
})?;
Ok(())
}
#[test]
fn provider_requiring_base_url_is_not_auto_inserted_from_env_key_only() -> anyhow::Result<()> {
with_env("WORKSPACEPROV_API_KEY", Some("sk-test"), || {
let mut config = Config::default();
let mut p = provider("workspaceprov");
p.api_base = None;
p.required_config = vec![RequiredConfig::ApiKey, RequiredConfig::BaseUrl];
apply_registry(&mut config, &data_with(vec![p], vec![]));
assert!(
!config.providers.contains_key("workspaceprov"),
"a provider that requires base_url must not be auto-added from the API key alone"
);
Ok(())
})?;
Ok(())
}
#[test]
fn listed_env_provider_without_key_is_marked_inactive() -> anyhow::Result<()> {
with_env("LISTEDENVPROV_API_KEY", None, || {
let mut config = Config::default();
config.providers.insert(
"listedenvprov".to_string(),
ProviderConfig {
active: true,
..ProviderConfig::default()
},
);
apply_registry(
&mut config,
&data_with(vec![provider("listedenvprov")], vec![]),
);
assert!(
!config.providers["listedenvprov"].active,
"an env-keyed provider with no key must be marked inactive"
);
Ok(())
})?;
Ok(())
}
#[test]
fn listed_oauth_provider_without_key_stays_active() -> anyhow::Result<()> {
with_env("OAUTHPROV_API_KEY", None, || {
let mut config = Config::default();
config.providers.insert(
"oauthprov".to_string(),
ProviderConfig {
active: true,
..ProviderConfig::default()
},
);
let mut p = provider("oauthprov");
p.auth = Some(RegistryAuth {
kind: RegistryAuthKind::Oauth,
env: None,
header: None,
extra_headers: None,
handler: Some("oauthprov".to_string()),
params: None,
});
apply_registry(&mut config, &data_with(vec![p], vec![]));
assert!(
config.providers["oauthprov"].active,
"an OAuth provider authenticates via login, so it stays active"
);
Ok(())
})?;
Ok(())
}
#[test]
fn private_provider_is_never_merged() -> anyhow::Result<()> {
with_env("PRIVATEPROV_API_KEY", Some("sk-test"), || {
let mut config = Config::default();
let mut p = provider("privateprov");
p.access = Some(RegistryAccess::Private);
apply_registry(&mut config, &data_with(vec![p], vec![]));
assert!(
!config.providers.contains_key("privateprov"),
"access=private ⇒ never merged, even with a credential set"
);
Ok(())
})?;
Ok(())
}
#[test]
fn legacy_byok_false_still_resolves_to_private() -> anyhow::Result<()> {
let mut p = provider("legacyprivate");
p.access = None;
p.byok = Some(false);
assert_eq!(p.access(), RegistryAccess::Private);
assert!(!p.is_public());
Ok(())
}
#[test]
fn empty_model_provider_does_not_auto_discover_from_registry() -> anyhow::Result<()> {
with_env("GATEWAYPROV_API_KEY", Some("sk-test"), || {
let mut config = Config::default();
let mut p = provider("gatewayprov");
p.models = Vec::new();
apply_registry(&mut config, &data_with(vec![p], vec![]));
let merged = config
.providers
.get("gatewayprov")
.context("empty-model provider with a credential is merged")?;
assert!(
!merged.auto_discover,
"registry dist is complete, so empty model lists are not runtime discovery feeds"
);
Ok(())
})?;
Ok(())
}
#[test]
fn registry_dist_catalog_hints_are_ignored_at_runtime() -> anyhow::Result<()> {
with_env("LEGACYGW_API_KEY", Some("sk-test"), || {
let data: RegistryData = serde_json::from_str(
r#"{
"providers": [{
"name": "legacygw",
"status": "active",
"api_base": "https://legacygw.example/v1",
"access": "api_key",
"byok": true,
"api_protocol": [{ "*": "openai" }],
"auto_sync": { "feed": "v1_models" },
"models": []
}],
"canonical": []
}"#,
)
.context("legacy dist shape parses")?;
let mut config = Config::default();
apply_registry(&mut config, &data);
let merged = config
.providers
.get("legacygw")
.context("legacy gateway with a credential is merged")?;
assert!(
!merged.auto_discover,
"registry dist catalog hints are maintainer metadata, not runtime discovery"
);
Ok(())
})?;
Ok(())
}
#[test]
fn inactive_status_provider_is_skipped() -> anyhow::Result<()> {
with_env("STAGINGPROV_API_KEY", Some("sk-test"), || {
let mut config = Config::default();
let mut p = provider("stagingprov");
p.status = "staging".to_string();
apply_registry(&mut config, &data_with(vec![p], vec![]));
assert!(!config.providers.contains_key("stagingprov"));
Ok(())
})?;
Ok(())
}
#[test]
fn user_declared_fields_are_not_overwritten() -> anyhow::Result<()> {
for active in [false, true] {
let mut config = Config::default();
let mut user = ProviderConfig {
api_base: "https://gateway.internal/v1".to_string(),
api_key: "sk-user".to_string(),
active,
..ProviderConfig::default()
};
user.models = vec![ProviderModel {
id: "custom/model".to_string(),
provider_model_id: None,
api_protocol: None,
rate_limits: None,
pricing: None,
pricing_by_protocol: std::collections::HashMap::new(),
token_limits: Default::default(),
input_token_counting: None,
capabilities: Vec::new(),
reasoning_effort: None,
compatibility: Default::default(),
}];
config.providers.insert("regtestprov".to_string(), user);
apply_registry(
&mut config,
&data_with(vec![provider("regtestprov")], vec![]),
);
let mut refreshed = provider("regtestprov");
refreshed.api_base = Some("https://new-catalog.invalid/v1".into());
apply_registry(&mut config, &data_with(vec![refreshed], vec![]));
let merged = &config.providers["regtestprov"];
assert_eq!(merged.active, active);
assert_eq!(merged.api_base, "https://gateway.internal/v1");
assert_eq!(merged.models.len(), 1, "user's model list is preserved");
assert_eq!(merged.models[0].id, "custom/model");
assert_eq!(merged.class, Some(ProviderClass::FirstPartyApi));
}
Ok(())
}
#[test]
fn bitrouter_cloud_merge_keeps_oauth_transport_without_catalog_discovery() -> anyhow::Result<()>
{
let mut config = Config::default();
config
.providers
.insert(BITROUTER_CLOUD_ID.to_string(), ProviderConfig::default());
let mut bitrouter = provider(BITROUTER_CLOUD_ID);
bitrouter.display_name = Some("BitRouter Cloud".to_string());
bitrouter.kind = Some(bitrouter_ai::catalog::types::RegistryKind::Cloud);
bitrouter.api_base = Some("https://api.bitrouter.ai/v1".to_string());
bitrouter.models = Vec::new();
bitrouter.auth = Some(RegistryAuth {
kind: RegistryAuthKind::Bearer,
env: Some("BITROUTER_API_KEY".to_string()),
header: None,
extra_headers: None,
handler: None,
params: None,
});
let data = data_with(
vec![bitrouter],
vec!["anthropic/claude-sonnet-4.6", "deepseek/deepseek-v3.2"],
);
apply_registry(&mut config, &data);
let cloud = &config.providers[BITROUTER_CLOUD_ID];
assert_eq!(cloud.class, Some(ProviderClass::BitrouterCloud));
assert_eq!(cloud.api_base, "https://api.bitrouter.ai/v1");
assert!(
!cloud.auto_discover,
"registry dist does not ask OSS to discover provider catalogs at runtime"
);
assert!(
cloud.models.is_empty(),
"OSS must not infer BitRouter Cloud serves every canonical registry model"
);
assert!(
cloud.active,
"a configured BitRouter Cloud provider may authenticate via OAuth, not only BITROUTER_API_KEY"
);
Ok(())
}
#[test]
fn disabled_registry_is_a_noop() -> anyhow::Result<()> {
with_env("REGTESTPROV_API_KEY", Some("sk-test"), || {
let mut config = Config::default();
config.registry.enabled = false;
apply_registry(
&mut config,
&data_with(vec![provider("regtestprov")], vec![]),
);
assert!(config.providers.is_empty());
Ok(())
})?;
Ok(())
}
#[test]
fn inherit_defaults_false_is_a_noop() -> anyhow::Result<()> {
with_env("REGTESTPROV_API_KEY", Some("sk-test"), || {
let mut config = Config {
inherit_defaults: false,
..Config::default()
};
apply_registry(
&mut config,
&data_with(vec![provider("regtestprov")], vec![]),
);
assert!(config.providers.is_empty());
Ok(())
})?;
Ok(())
}
#[test]
fn apply_registry_is_idempotent() -> anyhow::Result<()> {
with_env("REGTESTPROV_API_KEY", Some("sk-test"), || {
let data = data_with(
vec![provider("regtestprov")],
vec!["anthropic/claude-sonnet-4.6", "deepseek/deepseek-v3.2"],
);
let mut config = Config::default();
config
.providers
.insert(BITROUTER_CLOUD_ID.to_string(), ProviderConfig::default());
apply_registry(&mut config, &data);
let merged_models = config.providers["regtestprov"].models.len();
let cloud_models = config.providers[BITROUTER_CLOUD_ID].models.len();
apply_registry(&mut config, &data);
assert_eq!(config.providers["regtestprov"].models.len(), merged_models);
assert_eq!(
config.providers[BITROUTER_CLOUD_ID].models.len(),
cloud_models,
"re-running must not mutate the cloud catalog"
);
assert_eq!(cloud_models, 0);
Ok(())
})?;
Ok(())
}
#[test]
fn registry_protocol_tariffs_keep_missing_buckets_and_empty_overrides() -> anyhow::Result<()> {
let mut registry = provider("fixture");
let model = registry.models.first_mut().context("model missing")?;
model.id = "fixture/model".into();
model.provider_model_id = "native".into();
model.pricing_by_protocol.insert(
ApiProtocol::Decisions,
serde_json::from_value(
serde_json::json!({"endpoint_profile":"openai_global","input_tokens":{"no_cache":0.1},"output_tokens":{"text":0.0}}),
)?,
);
model.pricing_by_protocol.insert(
ApiProtocol::Responses,
serde_json::from_value(serde_json::json!({}))?,
);
let models = build_models(®istry);
let mapped = models.first().context("mapped model missing")?;
let native = mapped
.pricing_for(&ApiProtocol::Decisions)
.context("native override missing")?;
assert_eq!(
native.endpoint_profile,
Some(bitrouter_ai::catalog::types::PricingEndpointProfile::OpenaiGlobal)
);
assert_eq!(native.input_micro_usd_per_token, Some(0.1));
assert_eq!(native.cache_read_micro_usd_per_token, None);
assert_eq!(native.output_micro_usd_per_token, Some(0.0));
let empty = mapped
.pricing_for(&ApiProtocol::Responses)
.context("empty explicit override was dropped")?;
assert_eq!(empty.input_micro_usd_per_token, None);
assert_eq!(empty.output_micro_usd_per_token, None);
Ok(())
}
}