use std::collections::BTreeMap;
use anyhow::{Context, Result, ensure};
use bitrouter_ai::types::ApiProtocol;
use bitrouter_sdk::caller::CallerContext;
use bitrouter_sdk::config::{AccountStrategy, Config, ConfigRoutingTable};
use bitrouter_sdk::language_model::{
PipelineContext, PipelineRequest, RoutingTable, RoutingTarget,
};
use serde_json::{Value, json};
use super::control::BlockDefinition;
use super::rubric::digest;
use crate::policy_lock::PolicyRoutingSnapshot;
pub(super) async fn request_compatible(
config: &Config,
policies: &PolicyRoutingSnapshot,
route: &str,
ctx: &PipelineContext,
) -> Result<bool> {
let resolution = config.resolve_router(route)?;
let mut preview = PipelineContext::new(PipelineRequest::new(
route,
ctx.caller().clone(),
ctx.require_generation_prompt()?.clone(),
));
preview.apply_preset_overrides(&resolution.overrides)?;
let mut models = vec![resolution.clean_model.clone()];
if let Some(policy) = &resolution.policy {
let document = policies
.document()
.context("named policy snapshot is unavailable")?;
let variant_name = resolution
.variant
.as_ref()
.map(|variant| format!("{policy}:{variant}"));
let definition = variant_name
.as_ref()
.and_then(|name| document.policies.get(name))
.or_else(|| document.policies.get(policy))
.context("named policy is unavailable")?;
models = definition
.tiers
.values()
.map(|target| target.model().to_owned())
.collect();
}
let mut stable = config.clone();
for provider in stable.providers.values_mut() {
provider.account_strategy = AccountStrategy::Failover;
}
let table = ConfigRoutingTable::from_config(stable);
let mut prefs = resolution.prefs;
prefs.require_capabilities = preview.require_generation_prompt()?.required_capabilities();
prefs.inbound_protocol = ctx.inbound_protocol();
for model in models {
match table.route_resolved(&model, &prefs, ctx.caller()).await {
Ok(chain) if !chain.is_empty() => {
if !chain.iter().all(|target| {
config
.providers
.get(&target.provider_name)
.is_some_and(|provider| {
prefs.require_capabilities.iter().all(|capability| {
provider.model_supports_capability(&target.service_id, *capability)
})
})
}) {
return Ok(false);
}
}
_ => return Ok(false),
}
}
Ok(true)
}
pub(super) async fn block_digest(
config: &Config,
policies: &PolicyRoutingSnapshot,
definition: &BlockDefinition,
) -> Result<String> {
definition.validate()?;
let mut stable = config.clone();
for provider in stable.providers.values_mut() {
provider.account_strategy = AccountStrategy::Failover;
}
let table = ConfigRoutingTable::from_config(stable);
let mut routes = BTreeMap::new();
for rule in &definition.rules {
ensure!(
rule.selector.starts_with('@')
|| (rule.selector.starts_with("bitrouter/")
&& config.resolve_router(&rule.selector).is_ok())
|| config.models.contains_key(&rule.selector),
"an evolution matcher must name a configured router, preset, or virtual route"
);
for route in [&rule.baseline_route, &rule.challenger_route] {
if !routes.contains_key(route) {
routes.insert(
route.clone(),
route_contract(config, &table, policies, route).await?,
);
}
}
}
digest(&(
"evolution-routing-contract-v1",
&definition.rules,
routes,
format!("{:?}", config.upstream.timeouts),
&config.upstream.fallback_backoff_ms,
))
}
pub(super) async fn route_contract(
config: &Config,
table: &ConfigRoutingTable,
policies: &PolicyRoutingSnapshot,
route: &str,
) -> Result<Value> {
let resolution = config.resolve_router(route)?;
let mut models = BTreeMap::from([(resolution.clean_model.clone(), None)]);
let mut policy_contract = Value::Null;
if let Some(policy) = &resolution.policy {
let document = policies
.document()
.context("named policy snapshot is unavailable")?;
let variant_name = resolution
.variant
.as_ref()
.map(|variant| format!("{policy}:{variant}"));
let (name, definition) = variant_name
.as_ref()
.and_then(|name| document.policies.get_key_value(name))
.or_else(|| document.policies.get_key_value(policy))
.context("route references an unavailable named policy")?;
models = definition
.tiers
.values()
.map(|target| (target.model().to_owned(), target.effort()))
.collect();
policy_contract = json!({"name": name, "definition": definition,
"certificates": document.certificates.get(name), "mode": config.policy.mode});
}
let mut chains = BTreeMap::new();
for (model, effort) in models {
let mut protocols = Vec::new();
for protocol in [
None,
Some(ApiProtocol::ChatCompletions),
Some(ApiProtocol::Messages),
Some(ApiProtocol::Responses),
] {
let mut prefs = resolution.prefs.clone();
prefs.inbound_protocol = protocol.clone();
let chain = table
.route_resolved(&model, &prefs, &CallerContext::local())
.await?;
ensure!(!chain.is_empty(), "route contains an unavailable model");
let targets = chain
.iter()
.map(|target| target_contract(config, target))
.collect::<Result<Vec<_>>>()?;
protocols.push(
json!({"inbound": protocol.map(|p| p.as_str().to_owned()), "targets": targets}),
);
}
chains.insert(
model.clone(),
json!({"effort": effort, "protocols": protocols,
"virtual_strategy": config.models.get(&model).map(|v| format!("{:?}", v.strategy)),
"virtual_pricing": config.models.get(&model).map(|v| format!("{:?}", v.pricing))}),
);
}
Ok(json!({"policy": policy_contract, "models": chains,
"prompt_defaults_digest": digest(&(&resolution.overrides.system_prompt, &resolution.overrides.params))?,
"preferences": {"sort": resolution.prefs.sort, "tags": resolution.prefs.require_tags,
"only": resolution.prefs.only, "ignore": resolution.prefs.ignore}}))
}
fn target_contract(config: &Config, target: &RoutingTarget) -> Result<Value> {
let url = url::Url::parse(&target.api_base)?;
ensure!(
url.username().is_empty()
&& url.password().is_none()
&& url.query().is_none()
&& url.fragment().is_none(),
"evolution requires an endpoint without URL credentials, query or fragment"
);
let provider = config
.providers
.get(&target.provider_name)
.context("provider disappeared")?;
let metadata = provider.models.iter().find(|model| {
model.id == target.service_id
|| model.provider_model_id.as_deref() == Some(&target.service_id)
});
Ok(
json!({"provider": target.provider_name, "model": target.service_id,
"endpoint": url.as_str(), "protocol": target.api_protocol.as_str(),
"account": target.account_label, "account_strategy": format!("{:?}", provider.account_strategy),
"chat_token_limit_field": target.chat_token_limit_field,
"chat_supports_store": target.chat_supports_store,
"chat_supports_stream_options": target.chat_supports_stream_options,
"chat_google_extensions": target.chat_google_extensions,
"reasoning_effort": target.reasoning_effort,
"capabilities": metadata.map(|model| &model.capabilities),
"pricing": metadata.map(|model| format!("{:?}", model.pricing)),
"timeouts": format!("{:?}", provider.timeouts)}),
)
}