use std::collections::BTreeMap;
use std::sync::OnceLock;
use bitrouter_ai::types::{ApiProtocol, ProtocolList};
use bitrouter_sdk::config::ProviderClass;
use super::entry::{AuthScheme, ProtocolMapping, ProviderEntry};
use bitrouter_ai::catalog::types::{
Billing, RegistryAuth, RegistryAuthKind, RegistryKind, RegistryProvider,
};
const BITROUTER_TOML: &str = include_str!("../../providers/bitrouter.toml");
static REGISTRY: OnceLock<Vec<ProviderEntry>> = OnceLock::new();
pub fn all() -> &'static [ProviderEntry] {
REGISTRY
.get_or_init(|| {
load_builtins().unwrap_or_else(|error| {
tracing::error!(%error, "embedded Cloud provider defaults are invalid");
Vec::new()
})
})
.as_slice()
}
pub fn find(id: &str) -> Option<&'static ProviderEntry> {
all().iter().find(|e| e.id == id)
}
pub fn load_builtins() -> Result<Vec<ProviderEntry>, LoadError> {
let bitrouter: ProviderEntry =
toml::from_str(BITROUTER_TOML).map_err(|source| LoadError::Parse {
id: "bitrouter".to_string(),
source,
})?;
Ok(vec![bitrouter])
}
pub fn entry_from_registry(p: &RegistryProvider) -> Result<ProviderEntry, LoadError> {
let auth = match &p.auth {
Some(auth) => map_auth(&p.name, auth)?,
None => AuthScheme::Bearer {
env: p.env_credential_var().ok_or_else(|| LoadError::Snapshot {
message: format!("provider '{}' has no credential env var", p.name),
})?,
},
};
let api_base = p.api_base.clone().ok_or_else(|| LoadError::Snapshot {
message: format!("provider '{}' has no fixed api_base", p.name),
})?;
Ok(ProviderEntry {
id: p.name.clone(),
display_name: p.display_name.clone().unwrap_or_else(|| p.name.clone()),
api_base,
api_protocol: derive_protocol_mapping(p),
protocol_endpoints: p.protocol_endpoints.clone().unwrap_or_default(),
auth,
doc_url: p.doc_url.clone().unwrap_or_default(),
class: Some(derive_class(p)),
})
}
fn map_auth(provider: &str, auth: &RegistryAuth) -> Result<AuthScheme, LoadError> {
let missing = |field: &str| LoadError::Snapshot {
message: format!(
"provider '{provider}' {:?} auth missing `{field}`",
auth.kind
),
};
match auth.kind {
RegistryAuthKind::Bearer => Ok(AuthScheme::Bearer {
env: auth.env.clone().ok_or_else(|| missing("env"))?,
}),
RegistryAuthKind::Header => Ok(AuthScheme::Header {
header: auth.header.clone().ok_or_else(|| missing("header"))?,
env: auth.env.clone().ok_or_else(|| missing("env"))?,
extra_headers: auth.extra_headers.clone().unwrap_or_default(),
}),
RegistryAuthKind::Oauth => Ok(AuthScheme::Oauth {
handler: auth.handler.clone().ok_or_else(|| missing("handler"))?,
params: auth
.params
.as_ref()
.map(|params| {
params
.iter()
.filter_map(|(k, v)| {
toml::Value::try_from(v).ok().map(|tv| (k.clone(), tv))
})
.collect()
})
.unwrap_or_default(),
}),
RegistryAuthKind::Native => Ok(AuthScheme::Native {
handler: auth.handler.clone().ok_or_else(|| missing("handler"))?,
}),
}
}
fn derive_protocol_mapping(p: &RegistryProvider) -> ProtocolMapping {
let mut globs: BTreeMap<String, ProtocolList> = BTreeMap::new();
for entry in &p.api_protocol {
for (pattern, set) in entry {
globs.insert(pattern.clone(), set.to_protocol_list());
}
}
if !globs.is_empty() {
return single_or_per_model(globs);
}
let mut per_model: BTreeMap<String, ProtocolList> = BTreeMap::new();
for m in &p.models {
per_model.insert(m.id.clone(), m.api_protocol.to_protocol_list());
}
if per_model.is_empty() {
return ProtocolMapping::Single(ProtocolList(vec![ApiProtocol::ChatCompletions]));
}
let mut values = per_model.values();
if let Some(first) = values.next()
&& values.all(|v| v == first)
{
return ProtocolMapping::Single(first.clone());
}
ProtocolMapping::PerModel(per_model)
}
fn single_or_per_model(globs: BTreeMap<String, ProtocolList>) -> ProtocolMapping {
if globs.len() == 1
&& let Some(list) = globs.get("*")
{
return ProtocolMapping::Single(list.clone());
}
ProtocolMapping::PerModel(globs)
}
fn derive_class(p: &RegistryProvider) -> ProviderClass {
let kind = p.kind.unwrap_or(if p.community {
RegistryKind::ThirdParty
} else {
RegistryKind::FirstParty
});
match kind {
RegistryKind::Cloud => ProviderClass::BitrouterCloud,
RegistryKind::Gateway => ProviderClass::GatewaySubscription,
RegistryKind::ThirdParty => ProviderClass::ThirdPartyApi,
RegistryKind::FirstParty => {
if p.billing == Billing::Subscription {
ProviderClass::FirstPartySubscription
} else {
ProviderClass::FirstPartyApi
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
use bitrouter_ai::types::ApiProtocol;
#[test]
fn only_the_cloud_gateway_is_compiled_in() -> anyhow::Result<()> {
let entries = load_builtins().context("bitrouter.toml must parse")?;
assert_eq!(entries.len(), 1, "only the cloud gateway is compiled in");
assert_eq!(entries[0].id, "bitrouter");
Ok(())
}
#[test]
fn bitrouter_parses_with_bearer_env_var() -> anyhow::Result<()> {
let entry = find("bitrouter").context("`bitrouter` must be compiled in")?;
assert_eq!(entry.api_base, "https://api.bitrouter.ai/v1");
assert_eq!(entry.auth.env_var(), Some("BITROUTER_API_KEY"));
assert_eq!(
entry.api_protocol.resolve("gpt-4o"),
Some(vec![ApiProtocol::ChatCompletions])
);
assert_eq!(entry.class, Some(ProviderClass::BitrouterCloud));
Ok(())
}
#[test]
fn registry_providers_are_not_compiled_in() -> anyhow::Result<()> {
for id in [
"openai",
"anthropic",
"google",
"openai-codex",
"github-copilot",
"openrouter",
"opencode-zen",
"opencode-go",
"definitely-not-a-provider",
] {
assert!(find(id).is_none(), "{id} must not be compiled in");
}
Ok(())
}
fn reg(json: serde_json::Value) -> anyhow::Result<RegistryProvider> {
Ok(serde_json::from_value(json)?)
}
#[test]
fn entry_from_registry_maps_oauth_gateway() -> anyhow::Result<()> {
let provider = reg(serde_json::json!({
"name": "github-copilot",
"display_name": "GitHub Copilot",
"api_base": "https://api.githubcopilot.com",
"kind": "gateway",
"billing": "subscription",
"access": "local_oauth",
"status": "active",
"api_protocol": [
{ "claude-*": "anthropic" },
{ "gpt-5.5-codex": "responses" },
{ "*": "openai" }
],
"auth": {
"kind": "oauth",
"handler": "github-copilot",
"params": {
"client_id": "Ov23xxx",
"device_authorization_endpoint": "https://github.com/login/device/code",
"token_endpoint": "https://github.com/login/oauth/access_token"
}
},
"models": []
}))?;
let entry = entry_from_registry(&provider).context("maps")?;
assert_eq!(entry.id, "github-copilot");
assert_eq!(entry.class, Some(ProviderClass::GatewaySubscription));
assert_eq!(
entry.api_protocol.resolve("claude-sonnet-4.6"),
Some(vec![ApiProtocol::Messages])
);
assert_eq!(
entry.api_protocol.resolve("gpt-5.5-codex"),
Some(vec![ApiProtocol::Responses])
);
assert_eq!(
entry.api_protocol.resolve("gpt-4o"),
Some(vec![ApiProtocol::ChatCompletions])
);
let AuthScheme::Oauth { params, .. } = &entry.auth else {
anyhow::bail!("github-copilot must use an OAuth scheme");
};
assert!(params.contains_key("client_id"));
assert!(params.contains_key("device_authorization_endpoint"));
assert!(params.contains_key("token_endpoint"));
Ok(())
}
#[test]
fn entry_from_registry_collapses_per_model_protocol_set() -> anyhow::Result<()> {
let provider = reg(serde_json::json!({
"name": "openai",
"api_base": "https://api.openai.com/v1",
"kind": "first_party",
"status": "active",
"auth": { "kind": "bearer", "env": "OPENAI_API_KEY" },
"models": [
{ "id": "openai/gpt-5.5", "provider_model_id": "gpt-5.5",
"api_protocol": ["openai", "responses"] }
]
}))?;
let entry = entry_from_registry(&provider).context("maps")?;
assert_eq!(entry.auth.env_var(), Some("OPENAI_API_KEY"));
assert_eq!(entry.class, Some(ProviderClass::FirstPartyApi));
assert_eq!(
entry.api_protocol.resolve("gpt-5.5"),
Some(vec![ApiProtocol::ChatCompletions, ApiProtocol::Responses])
);
Ok(())
}
#[test]
fn entry_from_registry_uses_implicit_bearer_auth() -> Result<(), Box<dyn std::error::Error>> {
let provider: RegistryProvider = serde_json::from_value(serde_json::json!({
"name": "example-provider",
"api_base": "https://example.test/v1",
"status": "active",
"models": []
}))?;
let entry = entry_from_registry(&provider)?;
assert!(
matches!(entry.auth, AuthScheme::Bearer { env } if env == "EXAMPLE_PROVIDER_API_KEY")
);
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error("failed to parse provider entry '{id}': {source}")]
Parse {
id: String,
#[source]
source: toml::de::Error,
},
#[error("invalid registry provider: {message}")]
Snapshot {
message: String,
},
}