use std::sync::Arc;
use anyhow::{Context, Result};
use sea_orm::DatabaseConnection;
use bitrouter_sdk::App;
use bitrouter_sdk::PromptTransform;
use bitrouter_sdk::config::{Config, ConfigRoutingTable};
use bitrouter_sdk::language_model::protocol::OutboundDispatch;
use bitrouter_sdk::language_model::server_tools::advisor::AdvisorToolset;
use bitrouter_sdk::language_model::server_tools::approval::AllowAll;
use bitrouter_sdk::language_model::server_tools::config::ServerToolLoopConfig;
use bitrouter_sdk::language_model::server_tools::declarations::ServerToolDeclarationsHook;
use bitrouter_sdk::language_model::server_tools::declarations::forwarded_tools;
use bitrouter_sdk::language_model::server_tools::fusion::FusionToolset;
use bitrouter_sdk::language_model::server_tools::fusion::alias::FusionAliasConfig;
use bitrouter_sdk::language_model::server_tools::loop_controller::ServerToolLoop;
use bitrouter_sdk::language_model::server_tools::mcp_toolset::McpRouterToolset;
use bitrouter_sdk::language_model::server_tools::nested::{NestedRunner, PipelineNestedRunner};
use bitrouter_sdk::language_model::server_tools::sub_agent::SubAgentToolset;
use bitrouter_sdk::language_model::server_tools::toolset::{RouterToolset, ToolsetRegistry};
use bitrouter_sdk::language_model::server_tools::web_fetch::backend::WebFetchBackend;
use bitrouter_sdk::language_model::server_tools::web_fetch::config::{
DEFAULT_MAX_CONTENT_TOKENS, WebFetchBackendConfig, WebFetchSettings,
};
use bitrouter_sdk::language_model::server_tools::web_fetch::http::{
HttpFetchBackend, HttpFetchEngine,
};
use bitrouter_sdk::language_model::server_tools::web_fetch::toolset::WebFetchToolset;
use bitrouter_sdk::language_model::server_tools::web_search::backend::WebSearchBackend;
use bitrouter_sdk::language_model::server_tools::web_search::config::{
DEFAULT_MAX_RESULTS, WebSearchBackendConfig, WebSearchSettings,
};
use bitrouter_sdk::language_model::server_tools::web_search::http::{
HttpEngine, HttpSearchBackend,
};
use bitrouter_sdk::language_model::server_tools::web_search::nested::NestedSearchBackend;
use bitrouter_sdk::language_model::server_tools::web_search::toolset::WebSearchToolset;
use bitrouter_sdk::language_model::{AuthAppliers, HttpExecutor, HttpTimeouts, PipelineBuilder};
use bitrouter_sdk::mcp::aggregating_executor::AggregatingExecutor;
use bitrouter_sdk::mcp::caching_executor::{CacheTtls, CachingExecutor};
use bitrouter_sdk::mcp::config_routing::{ConfigMcpRoutingTable, McpServerAggregateConfig};
use bitrouter_sdk::mcp::rmcp_executor::RmcpExecutor;
use bitrouter_guardrails::{GuardrailConfig, GuardrailsPlugin};
use bitrouter_observe::OTEL_ENABLED;
use bitrouter_observe::otel::{
ContentCaptureMode, MetricsConfig, OtelConfig, OtelExporter, OtelObserveHook,
};
use bitrouter_sdk::MetricsRenderer;
use crate::auth::AuthHook;
use crate::daemon::{NoopObserveStatus, ObserveStatusPayload, ObserveStatusProvider};
use crate::metering::{ContextTier, MeteringRecorder, MeteringStore, ModelPricing, PricingTable};
use crate::policy::{PolicyHook, PolicyStore};
pub struct Assembled {
pub app: App,
pub db: DatabaseConnection,
pub policy_store: Arc<PolicyStore>,
pub policy_runtime: Arc<crate::policy_lock::PolicyRuntime>,
pub routing_table: Arc<ConfigRoutingTable>,
pub upstream_executor: Arc<HttpExecutor>,
pub observe: Arc<dyn ObserveStatusProvider>,
pub otel_exporter: Option<Arc<OtelExporter>>,
pub otel_init_error: Option<String>,
}
pub(crate) fn resolved_upstream_timeouts(
config: &Config,
) -> (
HttpTimeouts,
std::collections::HashMap<String, HttpTimeouts>,
) {
let global_timeouts = config.upstream.timeouts.apply_to(HttpTimeouts::default());
let provider_timeouts: std::collections::HashMap<String, HttpTimeouts> = config
.providers
.iter()
.filter_map(|(id, provider)| {
let resolved = provider.timeouts.apply_to(global_timeouts.clone());
(resolved != global_timeouts).then(|| (id.clone(), resolved))
})
.collect();
(global_timeouts, provider_timeouts)
}
struct OtelExporterStatus {
exporter: Arc<OtelExporter>,
}
#[async_trait::async_trait]
impl ObserveStatusProvider for OtelExporterStatus {
fn status(&self) -> ObserveStatusPayload {
let s = self.exporter.status();
ObserveStatusPayload {
compiled_in: s.compiled_in,
exporter_wired: s.exporter_wired,
endpoint: s.endpoint,
header_count: s.header_count,
service_name: s.service_name,
resource_attribute_count: s.resource_attribute_count,
sampler: s.sampler,
sampler_arg: s.sampler_arg,
metrics_enabled: s.metrics_enabled,
api_key_count: s.api_key_count,
api_key_cap: s.api_key_cap,
user_id_count: s.user_id_count,
user_id_cap: s.user_id_cap,
active_spans: s.active_spans,
}
}
async fn shutdown(&self) {
let exporter = self.exporter.clone();
if let Err(e) = tokio::task::spawn_blocking(move || exporter.shutdown()).await {
tracing::warn!(error = %e, "OTel exporter shutdown task panicked");
}
}
}
pub async fn build_app(config: &Config) -> Result<Assembled> {
build_app_with_path(config, None).await
}
pub async fn build_app_with_path(
config: &Config,
config_path: Option<&std::path::Path>,
) -> Result<Assembled> {
let db = crate::db::connect(&config.database.url)
.await
.with_context(|| format!("connecting to database {}", config.database.url))?;
crate::db::run_migrations(&db)
.await
.context("running database migrations")?;
#[cfg(unix)]
if let Some(path) = sqlite_file_path(&config.database.url) {
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(&path) {
let mut perms = meta.permissions();
perms.set_mode(0o600);
if let Err(e) = std::fs::set_permissions(&path, perms) {
tracing::warn!(?path, %e, "failed to chmod 0600 on sqlite db file");
}
}
}
let mut resolved = config.clone();
bitrouter_providers::apply_builtin_defaults(&mut resolved);
if let Ok(store) = bitrouter_providers::oauth::credential_store::CredentialStore::default_path()
{
bitrouter_providers::activate_stored_credential_providers(&mut resolved, &store);
}
bitrouter_sdk::config::discover_models(&mut resolved).await;
let routing_table = Arc::new(match config_path {
Some(path) => ConfigRoutingTable::from_config_with_path(resolved, path),
None => ConfigRoutingTable::from_config(resolved),
});
let routing_table_for_reload = routing_table.clone();
let auth_appliers = build_auth_appliers(config)?;
let (global_timeouts, provider_timeouts) = resolved_upstream_timeouts(config);
let executor = Arc::new(
HttpExecutor::with_provider_timeouts(
global_timeouts,
provider_timeouts,
OutboundDispatch::builtin(),
auth_appliers,
)
.context("building the upstream HTTP executor")?,
);
let executor_for_reload = executor.clone();
let pricing = Arc::new(build_pricing_table(config));
let metering_store = MeteringStore::new(db.clone());
let metering_store_for_policy = metering_store.clone();
let metering_store_for_recorder = metering_store.clone();
let pricing_for_recorder = pricing.clone();
let policy_store: Arc<PolicyStore> = Arc::new(load_policy_store(config).await?);
let policy_store_for_reload = policy_store.clone();
let guardrail_rules = build_guardrail_config(config)?
.compile()
.context("compiling guardrail patterns")?;
let metrics_renderer: Arc<dyn MetricsRenderer> = Arc::new(EmptyMetricsRenderer);
let (otel_exporter, otel_init_error): (Option<Arc<OtelExporter>>, Option<String>) =
match build_otel_config(config)? {
Some(plan) => {
let bearer: Option<Arc<dyn bitrouter_observe::otel::TelemetryBearer>> =
match plan.bearer_plan {
BearerPlan::LiveSource { warn_if_unmet } => {
let source = crate::cloud::cloud_bearer_source().await;
if source.is_none() && warn_if_unmet {
tracing::warn!(
"telemetry: attribution=account but no signed-in session is \
available — exporting anonymously (sign in with \
`bitrouter cloud login`)"
);
}
source
}
BearerPlan::StaticOnly => None,
};
match OtelExporter::new(plan.config, bearer) {
Ok(exporter) => (Some(Arc::new(exporter)), None),
Err(e) => (
None,
Some(format!("failed to initialise OpenTelemetry: {e}")),
),
}
}
None => (None, None),
};
let observe_provider: Arc<dyn ObserveStatusProvider> = match otel_exporter.clone() {
Some(exporter) => Arc::new(OtelExporterStatus { exporter }),
None => Arc::new(NoopObserveStatus {
compiled_in: OTEL_ENABLED,
}),
};
let otel_for_hook = otel_exporter.clone();
let otel_for_assembled = otel_exporter;
let mcp_aggregate_route = if config.mcp.aggregate.enabled {
Some(config.mcp.aggregate.route.clone())
} else {
None
};
if let Some(route) = mcp_aggregate_route.as_deref() {
let agg_last = route.trim_end_matches('/').rsplit('/').next().unwrap_or("");
if !agg_last.is_empty() && config.mcp_servers.keys().any(|k| k.as_str() == agg_last) {
anyhow::bail!(
"mcp_servers entry '{agg_last}' would be shadowed by the per-server mount at \
'{route}/{agg_last}' (derived from the aggregate route '{route}'). Rename the \
server or move the aggregate route. Note: this check only catches the \
last-segment overlap; axum mounts may still reject other shapes at startup."
);
}
}
let mcp_routing = if config.mcp_servers.is_empty() {
None
} else {
Some(Arc::new(
ConfigMcpRoutingTable::from_configs(config.mcp_servers.iter().map(|(k, v)| {
let agg = McpServerAggregateConfig {
aggregate: v.aggregate,
tool_prefix: v.tool_prefix.clone().unwrap_or_else(|| format!("{k}__")),
};
(k.clone(), v.clone(), agg)
}))
.context("building the MCP routing table from config.mcp_servers")?,
))
};
let mcp_executor = mcp_routing.as_ref().map(|_| {
let rmcp: Arc<RmcpExecutor> = Arc::new(RmcpExecutor::new());
let inner_for_cache: Arc<RmcpExecutor> = rmcp.clone();
if config.mcp.cache.enabled {
let ttls: CacheTtls = (&config.mcp.cache).into();
let cached: Arc<CachingExecutor<RmcpExecutor>> = Arc::new(
CachingExecutor::new(inner_for_cache, ttls)
.with_invalidation(rmcp.invalidation_receiver()),
);
Arc::new(AggregatingExecutor::new(cached)) as Arc<dyn bitrouter_sdk::mcp::Executor>
} else {
Arc::new(AggregatingExecutor::new(inner_for_cache))
as Arc<dyn bitrouter_sdk::mcp::Executor>
}
});
let server_tools_enabled = config.server_tools.fusion.is_some()
|| config.server_tools.advisor
|| config.server_tools.subagent
|| config.server_tools.web_search.is_some()
|| config.server_tools.web_fetch.is_some();
let nested_runner: Option<Arc<dyn NestedRunner>> = if server_tools_enabled {
let mut sub = PipelineBuilder::new();
sub.routing_table(routing_table.clone())
.executor(executor.clone())
.settlement_recorder(MeteringRecorder::new(
metering_store.clone(),
pricing.clone(),
));
let sub_pipeline = Arc::new(
sub.build()
.context("building the server-tool sub-completion pipeline")?,
);
Some(Arc::new(PipelineNestedRunner::new(sub_pipeline)))
} else {
None
};
let server_tool_loop =
build_server_tool_loop(config, &mcp_routing, &mcp_executor, nested_runner);
let fusion_alias: Option<Arc<dyn PromptTransform>> = config
.server_tools
.fusion
.as_ref()
.and_then(FusionAliasConfig::from_settings)
.map(|c| Arc::new(c) as Arc<dyn PromptTransform>);
let policy_table = crate::policy_table_router::PolicyTable::from_config(&config.policy_table);
let adequacy_ledger = match &policy_table {
Some(_) if config.policy_table.adequacy.enabled => {
let store = crate::adequacy::store::AdequacyStore::new(db.clone());
Some(Arc::new(
crate::adequacy::AdequacyLedger::load(&config.policy_table.adequacy, store).await,
))
}
_ => None,
};
let adequacy_pending = Arc::new(crate::adequacy::settlement::PendingAdequacyStore::default());
let policy_decision_recorder =
crate::workflow_state::decision::PolicyDecisionJsonlRecorder::from_env()
.map_err(anyhow::Error::from)?
.map(Arc::new);
if policy_decision_recorder.is_some() {
tracing::info!(
env = crate::workflow_state::decision::POLICY_DECISION_JSONL_ENV,
"policy decision JSONL recording enabled"
);
}
let policy_runtime = crate::policy_lock::PolicyRuntime::new(
config,
config_path,
db.clone(),
adequacy_pending.clone(),
policy_decision_recorder.clone(),
)
.await
.context("loading policy-lock.yaml")?;
let policy_runtime_for_selector = policy_runtime.clone();
let adequacy_settlement_pending = adequacy_pending.clone();
let db_for_hooks = db.clone();
let app = App::builder()
.skip_auth(config.server.skip_auth)
.metrics_renderer(metrics_renderer)
.language_model(move |lm| {
lm.routing_table(routing_table).executor(executor);
lm.model_selector(policy_runtime_for_selector);
if server_tools_enabled {
lm.pre_request_hook(ServerToolDeclarationsHook);
}
lm.pre_request_hook(AuthHook::new(db_for_hooks.clone()));
lm.pre_request_hook(PolicyHook::new(
policy_store.clone(),
Some(metering_store_for_policy),
));
if let Some(exporter) = otel_for_hook {
lm.observe_hook(OtelObserveHook::new(exporter));
}
lm.settlement_recorder(
crate::adequacy::settlement::AdequacySettlementRecorder::new(
adequacy_settlement_pending,
),
);
lm.settlement_recorder(MeteringRecorder::new(
metering_store_for_recorder,
pricing_for_recorder,
));
if let Some(server_loop) = server_tool_loop {
lm.server_tool_loop(server_loop);
}
});
let app = if guardrail_rules.is_empty() {
app
} else {
app.plugin(GuardrailsPlugin::with_static(guardrail_rules))
};
let app = match fusion_alias {
Some(transform) => app.prompt_transform(transform),
None => app,
};
let app = app.prompt_transform(
Arc::new(crate::claude_code::ClaudeCodeRouter) as Arc<dyn PromptTransform>
);
let app = match policy_table {
Some(table) => {
let has_adequacy = adequacy_ledger.is_some();
let mut router =
crate::policy_table_router::PolicyTableRouter::new(table, adequacy_ledger);
if let Some(recorder) = policy_decision_recorder {
router = router.with_shared_decision_recorder(recorder);
}
if has_adequacy {
router = router.with_pending_adequacy_store(adequacy_pending);
}
app.prompt_transform(Arc::new(router) as Arc<dyn PromptTransform>)
}
None => app,
};
let app = match (mcp_routing, mcp_executor) {
(Some(table), Some(exec)) => {
let app = app.mcp(move |m| {
m.routing_table(table).executor(exec);
});
if let Some(route) = mcp_aggregate_route {
app.mcp_aggregate_route(route)
} else {
app
}
}
_ => app,
};
let app = app.build().context("building the App")?;
Ok(Assembled {
app,
db,
policy_store: policy_store_for_reload,
policy_runtime,
routing_table: routing_table_for_reload,
upstream_executor: executor_for_reload,
observe: observe_provider,
otel_exporter: otel_for_assembled,
otel_init_error,
})
}
pub async fn merge_registry_into(config: &mut Config) {
if !config.inherit_defaults || !config.registry.enabled {
return;
}
let Some(data) = bitrouter_providers::registry::apply::load_or_cached(&config.registry).await
else {
bitrouter_providers::apply_builtin_defaults(config);
return;
};
bitrouter_providers::registry::apply::apply_registry(config, &data);
bitrouter_providers::apply_builtin_defaults(config);
}
fn build_server_tool_loop(
config: &Config,
mcp_routing: &Option<Arc<ConfigMcpRoutingTable>>,
mcp_executor: &Option<Arc<dyn bitrouter_sdk::mcp::Executor>>,
nested_runner: Option<Arc<dyn NestedRunner>>,
) -> Option<Arc<ServerToolLoop>> {
let settings = &config.server_tools;
let mut sets: Vec<Arc<dyn RouterToolset>> = Vec::new();
if !settings.mcp_servers.is_empty() {
if let (Some(routing), Some(executor)) = (mcp_routing, mcp_executor) {
let routing: Arc<dyn bitrouter_sdk::mcp::RoutingTable> = routing.clone();
for name in &settings.mcp_servers {
let Some(server) = config.mcp_servers.get(name) else {
tracing::warn!(server = %name,
"server_tools.mcp_servers names an MCP server absent from mcp_servers; skipping");
continue;
};
let prefix = server
.tool_prefix
.clone()
.unwrap_or_else(|| format!("{name}__"));
sets.push(Arc::new(McpRouterToolset::new(
executor.clone(),
routing.clone(),
name.clone(),
Some(prefix),
)));
}
} else {
tracing::warn!(
"server_tools.mcp_servers is set but no mcp_servers are configured; \
MCP server tools are disabled"
);
}
}
if let Some(runner) = &nested_runner {
if settings.advisor {
sets.push(Arc::new(AdvisorToolset::new(runner.clone())));
}
if settings.subagent {
sets.push(Arc::new(SubAgentToolset::new(runner.clone())));
}
if settings.fusion.is_some() {
sets.push(Arc::new(FusionToolset::new(runner.clone())));
}
}
if let Some(web_search) = &settings.web_search {
let backends = build_web_search_backends(web_search, &nested_runner);
if backends.is_empty() {
tracing::warn!(
"server_tools.web_search is set but no backend resolved (missing API keys?); \
web_search is disabled"
);
} else {
let max_results = web_search.max_results.unwrap_or(DEFAULT_MAX_RESULTS);
sets.push(Arc::new(WebSearchToolset::new(backends, max_results)));
}
}
if let Some(web_fetch) = &settings.web_fetch {
let backends = build_web_fetch_backends(web_fetch);
if backends.is_empty() {
tracing::warn!(
"server_tools.web_fetch is set but no backend resolved (missing API keys?); \
web_fetch is disabled"
);
} else {
let max_content_tokens = web_fetch
.max_content_tokens
.unwrap_or(DEFAULT_MAX_CONTENT_TOKENS);
sets.push(Arc::new(WebFetchToolset::new(backends, max_content_tokens)));
}
}
if sets.is_empty() {
return None;
}
let mut loop_config = ServerToolLoopConfig::default();
if let Some(max) = settings.max_iterations {
loop_config.max_iterations = max;
}
Some(Arc::new(ServerToolLoop::new(
ToolsetRegistry::new(sets),
loop_config,
Arc::new(AllowAll),
)))
}
fn build_web_search_backends(
settings: &WebSearchSettings,
nested_runner: &Option<Arc<dyn NestedRunner>>,
) -> Vec<Arc<dyn WebSearchBackend>> {
let needs_http = settings.backends.iter().any(|b| {
matches!(
b,
WebSearchBackendConfig::Parallel { .. }
| WebSearchBackendConfig::Exa { .. }
| WebSearchBackendConfig::Firecrawl { .. }
| WebSearchBackendConfig::Tavily { .. }
)
});
let http = needs_http.then(|| {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(20))
.build()
.unwrap_or_default()
});
let mut backends: Vec<Arc<dyn WebSearchBackend>> = Vec::new();
for entry in &settings.backends {
match entry {
WebSearchBackendConfig::Parallel { api_key, api_base }
| WebSearchBackendConfig::Exa { api_key, api_base }
| WebSearchBackendConfig::Firecrawl { api_key, api_base }
| WebSearchBackendConfig::Tavily { api_key, api_base } => {
let engine = match entry {
WebSearchBackendConfig::Exa { .. } => HttpEngine::Exa,
WebSearchBackendConfig::Firecrawl { .. } => HttpEngine::Firecrawl,
WebSearchBackendConfig::Tavily { .. } => HttpEngine::Tavily,
_ => HttpEngine::Parallel,
};
if let (Some(client), Some(key)) = (
&http,
resolve_byok_key(api_key, engine.env_var(), engine.name()),
) {
backends.push(Arc::new(HttpSearchBackend::new(
engine,
key,
api_base.clone(),
client.clone(),
)));
}
}
WebSearchBackendConfig::Native { name, model, tool } => {
if let Some(runner) = nested_runner {
backends.push(Arc::new(NestedSearchBackend::new(
name.clone().unwrap_or_else(|| "native".to_string()),
runner.clone(),
model.clone(),
forwarded_tools(std::slice::from_ref(tool)),
)));
} else {
tracing::warn!("web_search native backend needs a nested runner; skipping");
}
}
}
}
backends
}
fn build_web_fetch_backends(settings: &WebFetchSettings) -> Vec<Arc<dyn WebFetchBackend>> {
if settings.backends.is_empty() {
return Vec::new();
}
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(20))
.build()
.unwrap_or_default();
let mut backends: Vec<Arc<dyn WebFetchBackend>> = Vec::new();
for entry in &settings.backends {
let (engine, api_key, api_base) = match entry {
WebFetchBackendConfig::Exa { api_key, api_base } => {
(HttpFetchEngine::Exa, api_key, api_base)
}
WebFetchBackendConfig::Firecrawl { api_key, api_base } => {
(HttpFetchEngine::Firecrawl, api_key, api_base)
}
WebFetchBackendConfig::Tavily { api_key, api_base } => {
(HttpFetchEngine::Tavily, api_key, api_base)
}
};
if let Some(key) = resolve_byok_key(api_key, engine.env_var(), engine.name()) {
backends.push(Arc::new(HttpFetchBackend::new(
engine,
key,
api_base.clone(),
http.clone(),
)));
}
}
backends
}
fn resolve_byok_key(explicit: &Option<String>, env_var: &str, backend: &str) -> Option<String> {
if let Some(key) = explicit.as_ref().filter(|k| !k.is_empty()) {
return Some(key.clone());
}
match bitrouter_sdk::config::env_lookup(env_var) {
Some(key) if !key.is_empty() => Some(key),
_ => {
tracing::warn!(
backend,
env_var,
"server-tool backend has no API key (set the env var or `api_key`); skipping"
);
None
}
}
}
fn build_auth_appliers(config: &Config) -> Result<AuthAppliers> {
let mut appliers = AuthAppliers::new();
let store_path = bitrouter_providers::oauth::credential_store::CredentialStore::default_path()
.map(|s| s.path().to_path_buf())
.context("resolving credential store path")?;
crate::cloud::register_if_configured(config, &mut appliers)?;
if config.providers.contains_key("github-copilot") {
let applier = bitrouter_providers::copilot::CopilotAuthApplier::new(&store_path)
.context("building the github-copilot AuthApplier")?;
appliers.register("github-copilot", Arc::new(applier));
}
if config.providers.contains_key("anthropic") {
let applier = bitrouter_providers::anthropic::AnthropicApiKeyApplier::new(&store_path)
.context("building the anthropic AuthApplier")?;
appliers.register("anthropic", Arc::new(applier));
}
if config.providers.contains_key("claude-code") {
let applier = bitrouter_providers::claude_code::ClaudeCodeAuthApplier::new(&store_path)
.context("building the claude-code AuthApplier")?;
appliers.register("claude-code", Arc::new(applier));
}
if config.providers.contains_key("openai-codex") {
let applier = bitrouter_providers::codex::OpenAiCodexAuthApplier::new(&store_path)
.context("building the openai-codex AuthApplier")?;
appliers.register("openai-codex", Arc::new(applier));
}
if config.providers.contains_key("supergrok") {
let applier = bitrouter_providers::supergrok::SuperGrokAuthApplier::new(&store_path)
.context("building the supergrok AuthApplier")?;
appliers.register("supergrok", Arc::new(applier));
}
Ok(appliers)
}
fn build_pricing_table(config: &Config) -> PricingTable {
let mut table = PricingTable::new();
for (provider_id, provider) in &config.providers {
for model in &provider.models {
if let Some(pricing) = &model.pricing {
let mut model_pricing = ModelPricing::new(
pricing.input_micro_usd_per_token,
pricing.output_micro_usd_per_token,
);
model_pricing.context_tiers = pricing
.context_tiers
.iter()
.map(|t| ContextTier {
above_input_tokens: t.above_input_tokens,
input_micro_usd_per_token: Some(t.input_micro_usd_per_token),
output_micro_usd_per_token: Some(t.output_micro_usd_per_token),
})
.collect();
table.insert(provider_id.clone(), model.id.clone(), model_pricing);
}
}
}
table
}
async fn load_policy_store(config: &Config) -> Result<PolicyStore> {
let dir = config
.plugins
.get("bitrouter-policy")
.and_then(|c| c.get("policy_dir"))
.and_then(|v| v.as_str());
match dir {
Some(dir) => PolicyStore::load_dir(dir)
.await
.with_context(|| format!("loading policies from {dir}")),
None => Ok(PolicyStore::new()),
}
}
fn build_guardrail_config(config: &Config) -> Result<GuardrailConfig> {
let Some(value) = config.plugins.get("bitrouter-guardrails") else {
return Ok(GuardrailConfig::default());
};
serde_json::from_value(value.clone()).context("plugins.bitrouter-guardrails failed to parse")
}
struct EmptyMetricsRenderer;
impl MetricsRenderer for EmptyMetricsRenderer {
fn render(&self) -> String {
"# Prometheus metrics have been removed in favor of OpenTelemetry.\n".to_string()
+ "# Configure OTLP export via plugins.bitrouter-observe.otel\n"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BearerPlan {
LiveSource { warn_if_unmet: bool },
StaticOnly,
}
pub async fn build_otel_exporter_standalone(config: &Config) -> Option<Arc<OtelExporter>> {
let plan = match build_otel_config(config) {
Ok(Some(plan)) => plan,
Ok(None) => return None,
Err(e) => {
tracing::warn!("telemetry: invalid observe config, exporting disabled: {e:#}");
return None;
}
};
let bearer: Option<Arc<dyn bitrouter_observe::otel::TelemetryBearer>> = match plan.bearer_plan {
BearerPlan::LiveSource { warn_if_unmet } => {
let source = crate::cloud::cloud_bearer_source().await;
if source.is_none() && warn_if_unmet {
tracing::warn!(
"telemetry: attribution=account but no signed-in session is available — \
exporting anonymously (sign in with `bitrouter cloud login`)"
);
}
source
}
BearerPlan::StaticOnly => None,
};
match OtelExporter::new(plan.config, bearer) {
Ok(exporter) => Some(Arc::new(exporter)),
Err(e) => {
tracing::warn!("telemetry: failed to initialise OpenTelemetry: {e}");
None
}
}
}
struct OtelConfigPlan {
config: OtelConfig,
bearer_plan: BearerPlan,
}
fn build_otel_config(config: &Config) -> Result<Option<OtelConfigPlan>> {
let observe = config.plugins.get("bitrouter-observe");
if let Some(tel_value) = observe.and_then(|c| c.get("telemetry")) {
let opt_in = serde_json::from_value::<TelemetryOptIn>(tel_value.clone())
.context("plugins.bitrouter-observe.telemetry failed to parse")?;
if opt_in.enabled {
let install_id = crate::paths::install_id()
.map_err(|e| tracing::warn!("telemetry: could not resolve install id: {e:#}"))
.ok();
let has_explicit =
opt_in.bearer_token.is_some() || std::env::var("BITROUTER_TELEMETRY_TOKEN").is_ok();
let bearer_plan = telemetry_bearer_plan(opt_in.attribution, has_explicit);
let cfg = build_telemetry_otel_config(opt_in, install_id);
return Ok(Some(OtelConfigPlan {
config: cfg,
bearer_plan,
}));
}
}
if let Some(otel_value) = observe.and_then(|c| c.get("otel")) {
let cfg = serde_json::from_value::<OtelConfig>(otel_value.clone())
.context("plugins.bitrouter-observe.otel failed to parse")?;
return Ok(Some(OtelConfigPlan {
config: cfg,
bearer_plan: BearerPlan::StaticOnly,
}));
}
if let Some(endpoint) = observe
.and_then(|c| c.get("otlp_endpoint"))
.and_then(|v| v.as_str())
{
let cfg = OtelConfig {
endpoint: endpoint.to_string(),
..OtelConfig::default()
};
tracing::warn!(
"plugins.bitrouter-observe.otlp_endpoint is deprecated; switch to plugins.bitrouter-observe.otel",
);
return Ok(Some(OtelConfigPlan {
config: cfg,
bearer_plan: BearerPlan::StaticOnly,
}));
}
if std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok() {
return Ok(Some(OtelConfigPlan {
config: OtelConfig::default(),
bearer_plan: BearerPlan::StaticOnly,
}));
}
Ok(None)
}
fn telemetry_bearer_plan(attribution: TelemetryAttribution, has_explicit: bool) -> BearerPlan {
match attribution {
TelemetryAttribution::Anonymous => BearerPlan::StaticOnly,
_ if has_explicit => BearerPlan::StaticOnly,
TelemetryAttribution::Auto => BearerPlan::LiveSource {
warn_if_unmet: false,
},
TelemetryAttribution::Account => BearerPlan::LiveSource {
warn_if_unmet: true,
},
}
}
pub const DEFAULT_TELEMETRY_ENDPOINT: &str = "https://telemetry.bitrouter.ai";
#[derive(Debug, Clone, Copy, serde::Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum TelemetryLevel {
#[default]
Metadata,
Full,
}
#[derive(Debug, Clone, Copy, serde::Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum TelemetryAttribution {
#[default]
Auto,
Account,
Anonymous,
}
#[derive(Debug, Clone, serde::Deserialize, Default)]
#[serde(default)]
struct TelemetryOptIn {
enabled: bool,
endpoint: Option<String>,
level: TelemetryLevel,
bearer_token: Option<String>,
attribution: TelemetryAttribution,
}
fn otlp_traces_endpoint(endpoint: &str) -> String {
let trimmed = endpoint.trim_end_matches('/');
match trimmed.split_once("://") {
Some((_scheme, rest)) if !rest.contains('/') => format!("{trimmed}/v1/traces"),
_ => trimmed.to_string(),
}
}
fn build_telemetry_otel_config(opt_in: TelemetryOptIn, install_id: Option<String>) -> OtelConfig {
let content_capture = match opt_in.level {
TelemetryLevel::Metadata => ContentCaptureMode::Off,
TelemetryLevel::Full => ContentCaptureMode::Full,
};
let explicit = opt_in
.bearer_token
.or_else(|| std::env::var("BITROUTER_TELEMETRY_TOKEN").ok());
let bearer_token = match opt_in.attribution {
TelemetryAttribution::Anonymous => None,
TelemetryAttribution::Auto | TelemetryAttribution::Account => explicit,
};
let mut resource_attributes = std::collections::HashMap::new();
if let Some(id) = install_id {
resource_attributes.insert("bitrouter.install_id".to_string(), id);
}
resource_attributes.insert(
"$lib".to_string(),
concat!("bitrouter ", env!("CARGO_PKG_VERSION")).to_string(),
);
let endpoint = otlp_traces_endpoint(
opt_in
.endpoint
.as_deref()
.unwrap_or(DEFAULT_TELEMETRY_ENDPOINT),
);
OtelConfig {
endpoint,
content_capture,
bearer_token,
resource_attributes,
metrics: MetricsConfig {
enabled: false,
..MetricsConfig::default()
},
..OtelConfig::default()
}
}
#[cfg(test)]
mod telemetry_opt_in_tests {
use super::*;
#[test]
fn opt_in_full_uses_default_traces_endpoint_metrics_off_and_stamps_install_id() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: None,
level: TelemetryLevel::Full,
bearer_token: Some("bra_x".to_string()),
attribution: TelemetryAttribution::Auto,
};
let cfg = build_telemetry_otel_config(opt_in, Some("inst-1".to_string()));
assert_eq!(
cfg.endpoint,
format!("{DEFAULT_TELEMETRY_ENDPOINT}/v1/traces")
);
assert_eq!(cfg.content_capture, ContentCaptureMode::Full);
assert_eq!(cfg.bearer_token.as_deref(), Some("bra_x"));
assert!(!cfg.metrics.enabled);
assert_eq!(
cfg.resource_attributes
.get("bitrouter.install_id")
.map(String::as_str),
Some("inst-1")
);
}
#[test]
fn opt_in_metadata_honors_endpoint_override_and_no_install_id() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: Some("https://otel.example".to_string()),
level: TelemetryLevel::Metadata,
bearer_token: None,
attribution: TelemetryAttribution::Auto,
};
let cfg = build_telemetry_otel_config(opt_in, None);
assert_eq!(cfg.endpoint, "https://otel.example/v1/traces");
assert_eq!(cfg.content_capture, ContentCaptureMode::Off);
assert!(!cfg.resource_attributes.contains_key("bitrouter.install_id"));
}
#[test]
fn build_config_sets_lib_resource_attribute() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: None,
level: TelemetryLevel::Full,
bearer_token: None,
attribution: TelemetryAttribution::Auto,
};
let cfg = build_telemetry_otel_config(opt_in, None);
assert_eq!(
cfg.resource_attributes.get("$lib").map(String::as_str),
Some(concat!("bitrouter ", env!("CARGO_PKG_VERSION")))
);
}
#[test]
fn otlp_traces_endpoint_appends_only_when_no_path() {
assert_eq!(
otlp_traces_endpoint("https://telemetry.bitrouter.ai"),
"https://telemetry.bitrouter.ai/v1/traces"
);
assert_eq!(
otlp_traces_endpoint("https://host:4318/"),
"https://host:4318/v1/traces"
);
assert_eq!(
otlp_traces_endpoint("https://host/v1/traces"),
"https://host/v1/traces"
);
assert_eq!(
otlp_traces_endpoint("https://collector.example/otlp/custom"),
"https://collector.example/otlp/custom"
);
}
#[test]
fn opt_in_parses_and_defaults_off() {
let opt_in: TelemetryOptIn =
serde_json::from_value(serde_json::json!({"enabled": true, "level": "full"})).unwrap();
assert!(opt_in.enabled);
assert_eq!(opt_in.level, TelemetryLevel::Full);
let empty: TelemetryOptIn = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(!empty.enabled);
assert_eq!(empty.level, TelemetryLevel::Metadata);
}
#[test]
fn attribution_defaults_to_auto_and_parses_variants() {
let auto: TelemetryOptIn =
serde_json::from_value(serde_json::json!({"enabled": true})).unwrap();
assert_eq!(auto.attribution, TelemetryAttribution::Auto);
let anon: TelemetryOptIn = serde_json::from_value(
serde_json::json!({"enabled": true, "attribution": "anonymous"}),
)
.unwrap();
assert_eq!(anon.attribution, TelemetryAttribution::Anonymous);
let acct: TelemetryOptIn =
serde_json::from_value(serde_json::json!({"enabled": true, "attribution": "account"}))
.unwrap();
assert_eq!(acct.attribution, TelemetryAttribution::Account);
}
#[test]
fn bearer_plan_anonymous_is_static_only_even_with_explicit() {
assert_eq!(
telemetry_bearer_plan(TelemetryAttribution::Anonymous, true),
BearerPlan::StaticOnly
);
assert_eq!(
telemetry_bearer_plan(TelemetryAttribution::Anonymous, false),
BearerPlan::StaticOnly
);
}
#[test]
fn bearer_plan_explicit_token_is_static_only() {
assert_eq!(
telemetry_bearer_plan(TelemetryAttribution::Auto, true),
BearerPlan::StaticOnly
);
assert_eq!(
telemetry_bearer_plan(TelemetryAttribution::Account, true),
BearerPlan::StaticOnly
);
}
#[test]
fn bearer_plan_auto_builds_live_source_no_warn() {
assert_eq!(
telemetry_bearer_plan(TelemetryAttribution::Auto, false),
BearerPlan::LiveSource {
warn_if_unmet: false
}
);
}
#[test]
fn bearer_plan_account_builds_live_source_and_warns_when_unmet() {
assert_eq!(
telemetry_bearer_plan(TelemetryAttribution::Account, false),
BearerPlan::LiveSource {
warn_if_unmet: true
}
);
}
#[test]
fn build_config_no_explicit_token_leaves_bearer_unset() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: None,
level: TelemetryLevel::Full,
bearer_token: None,
attribution: TelemetryAttribution::Auto,
};
let cfg = build_telemetry_otel_config(opt_in, None);
assert!(cfg.bearer_token.is_none());
}
#[test]
fn build_config_anonymous_drops_explicit_bearer() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: None,
level: TelemetryLevel::Full,
bearer_token: Some("explicit".into()),
attribution: TelemetryAttribution::Anonymous,
};
let cfg = build_telemetry_otel_config(opt_in, None);
assert!(cfg.bearer_token.is_none());
}
#[test]
fn build_config_explicit_token_lands_as_static_bearer() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: None,
level: TelemetryLevel::Full,
bearer_token: Some("explicit".into()),
attribution: TelemetryAttribution::Auto,
};
let cfg = build_telemetry_otel_config(opt_in, None);
assert_eq!(cfg.bearer_token.as_deref(), Some("explicit"));
}
#[test]
fn build_config_account_no_token_exports_anonymously() {
let opt_in = TelemetryOptIn {
enabled: true,
endpoint: None,
level: TelemetryLevel::Full,
bearer_token: None,
attribution: TelemetryAttribution::Account,
};
let cfg = build_telemetry_otel_config(opt_in, None);
assert!(cfg.bearer_token.is_none());
}
}
#[cfg(unix)]
fn sqlite_file_path(url: &str) -> Option<std::path::PathBuf> {
let after_scheme = url
.strip_prefix("sqlite://")
.or_else(|| url.strip_prefix("sqlite:"))?;
let path = after_scheme.split('?').next().unwrap_or(after_scheme);
if path.is_empty() || path == ":memory:" {
return None;
}
Some(std::path::PathBuf::from(path))
}
#[cfg(all(test, unix))]
mod sqlite_path_tests {
use super::sqlite_file_path;
use std::path::PathBuf;
#[test]
fn parses_common_sqlite_urls() {
assert_eq!(
sqlite_file_path("sqlite:///var/lib/bitrouter.db"),
Some(PathBuf::from("/var/lib/bitrouter.db"))
);
assert_eq!(
sqlite_file_path("sqlite:bitrouter.db"),
Some(PathBuf::from("bitrouter.db"))
);
assert_eq!(
sqlite_file_path("sqlite:bitrouter.db?cache=shared"),
Some(PathBuf::from("bitrouter.db"))
);
assert_eq!(sqlite_file_path(":memory:"), None);
assert_eq!(sqlite_file_path("sqlite::memory:"), None);
assert_eq!(sqlite_file_path("postgres://u:p@host/bitrouter"), None);
assert_eq!(sqlite_file_path("mysql://u:p@host/bitrouter"), None);
}
}
#[cfg(test)]
mod otel_config_tests {
use super::{BearerPlan, Config, build_otel_config};
fn config_with_observe(observe: serde_json::Value) -> Config {
let mut config = Config::default();
config
.plugins
.insert("bitrouter-observe".to_string(), observe);
config
}
#[test]
fn malformed_otel_block_is_a_hard_error() {
let config = config_with_observe(serde_json::json!({
"otel": { "sampler": "not_a_real_sampler" }
}));
assert!(
build_otel_config(&config).is_err(),
"a malformed otel block must be a hard error",
);
}
#[test]
fn valid_otel_block_parses() {
let config = config_with_observe(serde_json::json!({
"otel": { "endpoint": "http://collector:4318" }
}));
let plan = build_otel_config(&config)
.expect("valid otel block is Ok")
.expect("valid otel block yields Some");
assert_eq!(plan.config.endpoint, "http://collector:4318");
assert_eq!(plan.bearer_plan, BearerPlan::StaticOnly);
}
#[test]
fn legacy_otlp_endpoint_shim_still_works() {
let config = config_with_observe(serde_json::json!({
"otlp_endpoint": "http://legacy:4318"
}));
let plan = build_otel_config(&config)
.expect("legacy shim is Ok")
.expect("legacy shim yields Some");
assert_eq!(plan.config.endpoint, "http://legacy:4318");
assert_eq!(plan.bearer_plan, BearerPlan::StaticOnly);
}
}
#[cfg(test)]
mod server_tools_tests {
use std::sync::Arc;
use bitrouter_sdk::language_model::server_tools::nested::{
NestedOutcome, NestedRequest, NestedRunner,
};
use bitrouter_sdk::language_model::server_tools::toolset::ToolContext;
use super::{Config, build_server_tool_loop};
struct NoopRunner;
#[async_trait::async_trait]
impl NestedRunner for NoopRunner {
async fn run(
&self,
req: NestedRequest,
_ctx: &ToolContext,
) -> std::result::Result<NestedOutcome, String> {
Ok(NestedOutcome {
model: req.model,
text: String::new(),
usage: Default::default(),
})
}
}
#[test]
fn no_loop_when_server_tools_unset() {
let config = Config::default();
assert!(build_server_tool_loop(&config, &None, &None, None).is_none());
}
#[test]
fn no_loop_when_named_but_mcp_stack_absent() {
let mut config = Config::default();
config.server_tools.mcp_servers = vec!["docs".to_string()];
assert!(build_server_tool_loop(&config, &None, &None, None).is_none());
}
#[test]
fn loop_built_for_a_nested_server_tool_even_without_mcp() {
let mut fusion_cfg = Config::default();
fusion_cfg.server_tools.fusion = Some(Default::default());
let runner: Arc<dyn NestedRunner> = Arc::new(NoopRunner);
assert!(build_server_tool_loop(&fusion_cfg, &None, &None, Some(runner)).is_some());
let mut advisor_cfg = Config::default();
advisor_cfg.server_tools.advisor = true;
let runner2: Arc<dyn NestedRunner> = Arc::new(NoopRunner);
assert!(build_server_tool_loop(&advisor_cfg, &None, &None, Some(runner2)).is_some());
}
#[test]
fn no_loop_when_runner_present_but_no_tool_enabled() {
let config = Config::default();
let runner: Arc<dyn NestedRunner> = Arc::new(NoopRunner);
assert!(build_server_tool_loop(&config, &None, &None, Some(runner)).is_none());
}
#[test]
fn web_search_nested_backend_builds_loop() {
use bitrouter_sdk::language_model::server_tools::web_search::config::{
WebSearchBackendConfig, WebSearchSettings,
};
let mut cfg = Config::default();
cfg.server_tools.web_search = Some(WebSearchSettings {
backends: vec![WebSearchBackendConfig::Native {
name: None,
model: "anthropic/claude-opus-4.8".to_string(),
tool: serde_json::json!({ "type": "anthropic:web_search_20250305" }),
}],
max_results: None,
});
let runner: Arc<dyn NestedRunner> = Arc::new(NoopRunner);
assert!(build_server_tool_loop(&cfg, &None, &None, Some(runner)).is_some());
}
#[test]
fn web_search_http_backend_with_explicit_key_builds_loop_without_runner() {
use bitrouter_sdk::language_model::server_tools::web_search::config::{
WebSearchBackendConfig, WebSearchSettings,
};
let mut cfg = Config::default();
cfg.server_tools.web_search = Some(WebSearchSettings {
backends: vec![WebSearchBackendConfig::Exa {
api_key: Some("explicit-key".to_string()),
api_base: None,
}],
max_results: Some(3),
});
assert!(build_server_tool_loop(&cfg, &None, &None, None).is_some());
}
#[test]
fn web_fetch_http_backend_with_explicit_key_builds_loop_without_runner() {
use bitrouter_sdk::language_model::server_tools::web_fetch::config::{
WebFetchBackendConfig, WebFetchSettings,
};
let mut cfg = Config::default();
cfg.server_tools.web_fetch = Some(WebFetchSettings {
backends: vec![WebFetchBackendConfig::Exa {
api_key: Some("explicit-key".to_string()),
api_base: None,
}],
max_content_tokens: None,
});
assert!(build_server_tool_loop(&cfg, &None, &None, None).is_some());
}
#[test]
fn assembles_web_fetch_exa_backend_from_explicit_key() {
use bitrouter_sdk::language_model::server_tools::web_fetch::config::{
WebFetchBackendConfig, WebFetchSettings,
};
let backends = super::build_web_fetch_backends(&WebFetchSettings {
backends: vec![WebFetchBackendConfig::Exa {
api_key: Some("explicit-key".to_string()),
api_base: None,
}],
max_content_tokens: None,
});
assert_eq!(backends.len(), 1);
assert_eq!(backends[0].name(), "exa");
}
}