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::invocation;
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_sdk::MetricsRenderer;
use bitrouter_telemetry::OTEL_ENABLED;
use bitrouter_telemetry::otel::{
ContentCaptureMode, MetricsConfig, OtelConfig, OtelExporter, OtelObserveHook,
};
use crate::acp_runtime::AcpRuntime;
use crate::auth::AuthHook;
use crate::continuation::{ContinuationKeySource, ContinuationRegistry, ContinuationRuntime};
use crate::daemon::{NoopObserveStatus, ObserveStatusPayload, ObserveStatusProvider};
use crate::eval::EvalService;
use crate::eval::settlement::{EvalSettlementRecorder, PendingEvalDecisionStore};
use crate::eval::store::EvalStore;
use crate::metering::{ContextTier, MeteringRecorder, MeteringStore, ModelPricing, PricingTable};
use crate::policy::{PolicyHook, PolicyStore};
use crate::session_identity::SessionContextHook;
use crate::trajectory::publisher::TrajectoryOutboxPublisher;
use crate::trajectory::settlement::TrajectorySettlementRecorder;
use crate::trajectory::store::TrajectoryStore;
use crate::trajectory::{canonical::Canonicalizer, correlation::TrajectoryRuntime};
use crate::workflow_state::response_observer::PredictiveResponseObserver;
pub struct Assembled {
pub app: App,
pub request_checks: Arc<crate::request_checks::RequestCheckRuntime>,
pub db: DatabaseConnection,
pub acp_runtime: Arc<AcpRuntime>,
pub policy_store: Arc<PolicyStore>,
pub policy_runtime: Arc<crate::policy_lock::PolicyRuntime>,
pub evolution: crate::evolution::runtime::EvolutionRuntime,
pub eval_service: EvalService,
pub continuation_registry: ContinuationRegistry,
#[cfg(test)]
pub(crate) pending_eval_decisions: PendingEvalDecisionStore,
#[cfg(test)]
pub(crate) response_observer: PredictiveResponseObserver,
pub trajectory_outbox_publisher: Option<TrajectoryOutboxPublisher>,
pub routing_table: Arc<ConfigRoutingTable>,
pub upstream_executor: Arc<HttpExecutor>,
pub policy_table_router: Option<Arc<crate::policy_table_router::PolicyTableRouter>>,
pub observe: Arc<dyn ObserveStatusProvider>,
pub otel_exporter: Option<Arc<OtelExporter>>,
pub otel_init_error: Option<String>,
pub ignored_config: Vec<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 const KNOWN_PLUGIN_IDS: &[&str] = &[
"bitrouter-guardrails",
"bitrouter-policy",
"bitrouter-telemetry",
];
const REMOVED_PLUGIN_SUBKEYS: &[(&str, &str, &str)] = &[(
"bitrouter-telemetry",
"otlp_endpoint",
"removed with the `plugins.bitrouter-observe` rename; use `otel: { endpoint: … }`",
)];
const RENAMED_ENV_VARS: &[(&str, &str)] = &[
(
"BITROUTER_OBSERVE_CONTENT_CAPTURE",
"BITROUTER_TELEMETRY_CONTENT_CAPTURE",
),
(
"BITROUTER_OBSERVE_CONTENT_ATTR_MAX_BYTES",
"BITROUTER_TELEMETRY_CONTENT_ATTR_MAX_BYTES",
),
];
pub fn unknown_plugin_ids(config: &Config) -> Vec<String> {
let mut unknown: Vec<String> = config
.plugins
.keys()
.filter(|id| !KNOWN_PLUGIN_IDS.contains(&id.as_str()))
.cloned()
.collect();
unknown.sort();
unknown
}
pub fn ignored_config_file_warnings(config: &Config) -> Vec<String> {
let known = KNOWN_PLUGIN_IDS.join(", ");
unknown_plugin_ids(config)
.into_iter()
.map(|id| {
format!("plugins.{id} is not read by this binary and is ignored; known ids: {known}")
})
.chain(REMOVED_PLUGIN_SUBKEYS.iter().filter_map(|(id, key, note)| {
config
.plugins
.get(*id)
.and_then(|block| block.get(*key))
.map(|_| format!("plugins.{id}.{key} is ignored — {note}"))
}))
.collect()
}
pub fn ignored_config_warnings(config: &Config) -> Vec<String> {
ignored_config_file_warnings(config)
.into_iter()
.chain(renamed_env_warnings(|name| {
std::env::var_os(name).is_some()
}))
.collect()
}
fn renamed_env_warnings(is_set: impl Fn(&str) -> bool) -> Vec<String> {
RENAMED_ENV_VARS
.iter()
.filter(|(old, new)| is_set(old) && !is_set(new))
.map(|(old, new)| format!("{old} was renamed to {new}; the old name is ignored"))
.collect()
}
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> {
config.validate_router_config()?;
let request_checks = Arc::new(crate::request_checks::RequestCheckRuntime::activate(
config,
)?);
let ignored_config = ignored_config_warnings(config);
let fusion_alias = build_fusion_alias(config)?;
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")?;
let acp_runtime = Arc::new(AcpRuntime::new());
let cloud_manager = crate::cloud::default_manager()?;
let auth_appliers = build_auth_appliers(config, Arc::clone(&cloud_manager))?;
let runtime_home = match config_path.and_then(std::path::Path::parent) {
Some(home) => home.to_path_buf(),
None => std::env::current_dir().context("resolve continuation key home")?,
};
let continuation_registry = ContinuationRegistry::new(
db.clone(),
ContinuationKeySource::lazy(runtime_home),
config.continuation.retention_days,
config.continuation.prune_batch_size,
)?;
let continuation_pruned = continuation_registry.prune(chrono::Utc::now()).await?;
if continuation_pruned > 0 {
tracing::info!(
deleted_rows = continuation_pruned,
"pruned expired provider continuations at startup"
);
}
continuation_registry.start_reconciler();
let continuation_runtime = ContinuationRuntime::with_auth_appliers(
continuation_registry.clone(),
auth_appliers.clone(),
);
#[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 continuation_for_pre_request = continuation_runtime.clone();
let continuation_for_route = continuation_runtime.clone();
let continuation_for_finalization = continuation_runtime;
let (global_timeouts, provider_timeouts) = resolved_upstream_timeouts(config);
let mut dispatch = OutboundDispatch::builtin();
if config
.providers
.contains_key(bitrouter_providers::antigravity::PROVIDER_ID)
{
bitrouter_providers::antigravity::protocol::register(&mut dispatch);
}
let executor = Arc::new(
HttpExecutor::with_provider_timeouts(
global_timeouts,
provider_timeouts,
dispatch,
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_telemetry::otel::TelemetryBearer>> =
match plan.bearer_plan {
BearerPlan::LiveSource { warn_if_unmet } => {
let source = crate::cloud::cloud_bearer_source(
Arc::clone(&cloud_manager),
plan.config.endpoint.clone(),
)
.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 `{} \
cloud login`)",
invocation::name()
);
}
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().with_protocol_version(
bitrouter_sdk::mcp::upstream_protocol_version(config.mcp.upstream_protocol),
));
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())
.with_reconciliation_provider("bitrouter"),
);
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 mut effective_policy_table_config = config.policy_table.clone();
effective_policy_table_config.adequacy = config
.policy
.mode
.apply_to_adequacy(&config.policy_table.adequacy);
let policy_table =
crate::policy_table_router::PolicyTable::from_config(&effective_policy_table_config);
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 pending_eval_decisions = PendingEvalDecisionStore::default();
let eval_store = EvalStore::new(db.clone());
let eval_service = EvalService::new(eval_store.clone(), config.eval.clone());
let trajectory_services = if config.trajectory.enabled {
let trajectory_store = TrajectoryStore::new(db.clone());
let retention = chrono::TimeDelta::try_days(i64::from(config.trajectory.retention_days))
.ok_or_else(|| anyhow::anyhow!("trajectory retention_days exceeds time range"))?;
let cutoff = chrono::Utc::now()
.checked_sub_signed(retention)
.ok_or_else(|| anyhow::anyhow!("trajectory retention cutoff exceeds time range"))?
.to_rfc3339();
let pruned = trajectory_store
.prune_before(&cutoff, false, config.trajectory.outbox_batch_size)
.await?;
if pruned != crate::trajectory::store::PruneSummary::default() {
tracing::info!(
cutoff,
delivered_outbox_rows = pruned.delivered_outbox_rows,
episode_rows = pruned.episode_rows,
event_rows = pruned.event_rows,
request_rows = pruned.request_rows,
"pruned retained trajectory history at startup"
);
}
let publisher = TrajectoryOutboxPublisher::new(
trajectory_store.clone(),
eval_store.clone(),
config.eval.clone(),
config.trajectory.outbox_batch_size,
)?;
let startup_publication = publisher.drain_pending().await?;
if startup_publication.attempted > 0 {
tracing::info!(
attempted = startup_publication.attempted,
delivered = startup_publication.delivered,
failed = startup_publication.failed,
"drained durable trajectory evaluation outbox at startup"
);
}
let home = match config_path.and_then(std::path::Path::parent) {
Some(home) => home.to_path_buf(),
None => std::env::current_dir().context("resolve trajectory key home")?,
};
let correlation_key = crate::paths::get_or_create_correlation_key(&home)?;
let runtime = Arc::new(TrajectoryRuntime::new(
trajectory_store.clone(),
Canonicalizer::new(correlation_key.clone()),
));
let settlement =
TrajectorySettlementRecorder::new(trajectory_store, publisher.clone(), correlation_key);
Some((runtime, settlement, publisher))
} else {
None
};
let trajectory_runtime = trajectory_services
.as_ref()
.map(|(runtime, _, _)| runtime.clone());
let trajectory_for_eval = trajectory_services
.as_ref()
.map(|(_, settlement, _)| settlement.clone());
let trajectory_outbox_publisher = trajectory_services
.as_ref()
.map(|(_, _, publisher)| publisher.clone());
let policy_runtime = crate::policy_lock::PolicyRuntime::new(
config,
config_path,
db.clone(),
policy_decision_recorder.clone(),
pending_eval_decisions.clone(),
trajectory_runtime,
)
.await
.context("loading policy-lock.yaml")?;
let policy_runtime_for_selector = policy_runtime.clone();
let evolution = crate::evolution::runtime::EvolutionRuntime::new(
db.clone(),
routing_table.clone(),
policy_runtime.clone(),
);
let evolution_for_hooks = evolution.clone();
let judge_costs = crate::evolution::costs::JudgeCosts::new(db.clone());
#[cfg(test)]
let pending_eval_decisions_for_tests = pending_eval_decisions.clone();
let response_observer = PredictiveResponseObserver::new(pending_eval_decisions.clone());
#[cfg(test)]
let response_observer_for_tests = response_observer.clone();
let eval_store_for_recorder = eval_service.store().clone();
let pricing_for_eval = pricing.clone();
let db_for_hooks = db.clone();
let db_for_mcp_auth = db.clone();
let acp_runtime_for_session = Arc::clone(&acp_runtime);
let request_checks_for_pipeline = request_checks.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.request_receipt_store(request_checks_for_pipeline.receipts());
lm.request_checker_runner(request_checks_for_pipeline);
lm.fallback_backoff(
config
.upstream
.fallback_backoff_ms
.iter()
.copied()
.map(std::time::Duration::from_millis),
);
lm.model_selector(policy_runtime_for_selector);
lm.route_hook(continuation_for_route);
lm.route_hook(crate::policy_lock::PredictiveSingleTargetRouteHook);
lm.route_hook(evolution_for_hooks.clone());
lm.required_finalizer(continuation_for_finalization);
if server_tools_enabled {
lm.pre_resolution_hook(ServerToolDeclarationsHook);
}
lm.pre_resolution_hook(judge_costs.clone());
lm.pre_resolution_hook(AuthHook::new(db_for_hooks.clone()));
lm.pre_resolution_hook(SessionContextHook::new(acp_runtime_for_session));
lm.pre_resolution_hook(continuation_for_pre_request);
lm.router_preparation_hook(evolution_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.observe_hook(response_observer);
lm.observe_hook(evolution_for_hooks.clone());
lm.observe_hook(judge_costs.clone());
lm.settlement_recorder(
MeteringRecorder::new(metering_store_for_recorder, pricing_for_recorder)
.with_reconciliation_provider("bitrouter"),
);
lm.settlement_recorder(evolution_for_hooks);
lm.settlement_recorder(judge_costs);
let eval_recorder = EvalSettlementRecorder::new(
eval_store_for_recorder,
pending_eval_decisions,
pricing_for_eval,
);
let eval_recorder = match trajectory_for_eval {
Some(trajectory) => eval_recorder.with_trajectory(trajectory),
None => eval_recorder,
};
lm.settlement_recorder(eval_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 = app.prompt_transform(Arc::new(crate::codex_router::CodexRouter::new(Arc::clone(
&routing_table_for_reload,
))) as Arc<dyn PromptTransform>);
let (app, policy_table_router) = match policy_table {
Some(table) => {
let mut router = crate::policy_table_router::PolicyTableRouter::new(table);
if let Some(recorder) = policy_decision_recorder {
router = router.with_shared_decision_recorder(recorder);
}
let router = Arc::new(router);
(
app.prompt_transform(Arc::clone(&router) as Arc<dyn PromptTransform>),
Some(router),
)
}
None => (app, None),
};
let app = match (mcp_routing, mcp_executor) {
(Some(table), Some(exec)) => {
let app = app.mcp(move |m| {
m.routing_table(table)
.executor(exec)
.pre_request_hook(AuthHook::new(db_for_mcp_auth));
});
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 {
request_checks,
app,
db,
acp_runtime,
policy_store: policy_store_for_reload,
policy_runtime,
evolution,
eval_service,
continuation_registry,
#[cfg(test)]
pending_eval_decisions: pending_eval_decisions_for_tests,
#[cfg(test)]
response_observer: response_observer_for_tests,
trajectory_outbox_publisher,
routing_table: routing_table_for_reload,
upstream_executor: executor_for_reload,
policy_table_router,
observe: observe_provider,
otel_exporter: otel_for_assembled,
otel_init_error,
ignored_config,
})
}
fn build_fusion_alias(config: &Config) -> Result<Option<Arc<dyn PromptTransform>>> {
let Some(settings) = config.server_tools.fusion.as_ref() else {
return Ok(None);
};
FusionAliasConfig::validate_settings(settings)?;
Ok(FusionAliasConfig::from_settings(settings)
.map(|alias| Arc::new(alias) as Arc<dyn PromptTransform>))
}
pub async fn merge_registry_into(config: &mut Config) {
if !config.inherit_defaults || !config.registry.enabled {
return;
}
crate::bundled_registry::enable_logged_in(config);
let Some(data) = crate::bundled_registry::load(&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,
cloud_manager: Arc<bitrouter_providers::hosted::account::manager::CredentialManager>,
) -> 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, cloud_manager)?;
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));
}
if config
.providers
.contains_key(bitrouter_providers::antigravity::PROVIDER_ID)
{
let applier = bitrouter_providers::antigravity::AntigravityAuthApplier::new(&store_path)
.context("building the google-ai AuthApplier")?;
appliers.register(
bitrouter_providers::antigravity::PROVIDER_ID,
Arc::new(applier),
);
}
Ok(appliers)
}
pub(crate) 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::cache_aware(
pricing.input_micro_usd_per_token,
pricing.cache_read_micro_usd_per_token,
pricing.cache_write_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: t.input_micro_usd_per_token,
cache_read_micro_usd_per_token: t.cache_read_micro_usd_per_token,
cache_write_micro_usd_per_token: t.cache_write_micro_usd_per_token,
output_micro_usd_per_token: t.output_micro_usd_per_token,
})
.collect();
table.insert(provider_id.clone(), model.id.clone(), model_pricing.clone());
if let Some(native_id) = model.provider_model_id.as_deref()
&& native_id != model.id
{
table.insert(provider_id.clone(), native_id, model_pricing);
}
}
}
}
table
}
#[cfg(test)]
mod pricing_assembly_tests {
use super::build_pricing_table;
#[test]
fn provider_native_model_id_reuses_registry_pricing() -> anyhow::Result<()> {
let config = bitrouter_sdk::config::parse_with(
r#"
providers:
example:
api_base: https://example.test/v1
models:
- id: example/canonical-model
provider_model_id: native-model
pricing:
input_micro_usd_per_token: 1.25
output_micro_usd_per_token: 5.0
"#,
|_| None,
)?;
let pricing = build_pricing_table(&config);
assert_eq!(
pricing
.resolve("example", "example/canonical-model")
.and_then(|entry| entry.input_micro_usd_per_token),
Some(1.25)
);
assert_eq!(
pricing
.resolve("example", "native-model")
.and_then(|entry| entry.input_micro_usd_per_token),
Some(1.25)
);
Ok(())
}
}
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-telemetry.otel\n"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BearerPlan {
LiveSource { warn_if_unmet: bool },
StaticOnly,
}
pub(crate) async fn build_otel_exporter_standalone_with_credentials(
config: &Config,
cloud_credentials: &crate::cloud::StandaloneCloudCredentials,
) -> 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_telemetry::otel::TelemetryBearer>> = match plan.bearer_plan
{
BearerPlan::LiveSource { warn_if_unmet } => {
let source = cloud_credentials
.telemetry_bearer(&plan.config.endpoint)
.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 `{} cloud login`)",
invocation::name()
);
}
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 telemetry = config.plugins.get("bitrouter-telemetry");
if let Some(tel_value) = telemetry.and_then(|c| c.get("telemetry")) {
let opt_in = serde_json::from_value::<TelemetryOptIn>(tel_value.clone())
.context("plugins.bitrouter-telemetry.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) = telemetry.and_then(|c| c.get("otel")) {
let cfg = serde_json::from_value::<OtelConfig>(otel_value.clone())
.context("plugins.bitrouter-telemetry.otel failed to parse")?;
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_telemetry(telemetry: serde_json::Value) -> Config {
let mut config = Config::default();
config
.plugins
.insert("bitrouter-telemetry".to_string(), telemetry);
config
}
#[test]
fn malformed_otel_block_is_a_hard_error() {
let config = config_with_telemetry(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_telemetry(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 the_v0_otlp_endpoint_shim_is_gone() {
let config = config_with_telemetry(serde_json::json!({
"otlp_endpoint": "http://legacy:4318"
}));
assert!(
build_otel_config(&config)
.expect("an unknown sub-key is not an error")
.is_none(),
"otlp_endpoint must no longer wire an exporter"
);
}
#[test]
fn the_old_plugin_id_is_reported_rather_than_read() {
let mut config = Config::default();
config.plugins.insert(
"bitrouter-observe".to_string(),
serde_json::json!({ "otel": { "endpoint": "http://collector:4318" } }),
);
assert!(
build_otel_config(&config)
.expect("an unread plugin block is not an error")
.is_none()
);
assert_eq!(
super::unknown_plugin_ids(&config),
vec!["bitrouter-observe".to_string()]
);
}
#[test]
fn a_typoed_guardrails_id_is_reported() {
let mut config = Config::default();
config.plugins.insert(
"bitrouter-guardrail".to_string(),
serde_json::json!({ "custom_patterns": [] }),
);
assert_eq!(
super::unknown_plugin_ids(&config),
vec!["bitrouter-guardrail".to_string()]
);
}
#[test]
fn a_stale_env_name_warns_until_the_new_one_is_set() {
let warn = |set: &[&str]| super::renamed_env_warnings(|name| set.contains(&name));
assert!(warn(&[]).is_empty(), "nothing set, nothing to say");
assert_eq!(
warn(&["BITROUTER_OBSERVE_CONTENT_CAPTURE"]).len(),
1,
"the old name alone is what an operator needs telling about"
);
assert!(
warn(&["BITROUTER_TELEMETRY_CONTENT_CAPTURE"]).is_empty(),
"the new name alone is the migrated state"
);
assert!(
warn(&[
"BITROUTER_OBSERVE_CONTENT_CAPTURE",
"BITROUTER_TELEMETRY_CONTENT_CAPTURE",
])
.is_empty(),
"both set means migrated with a stale export left behind — not a line per start"
);
}
#[test]
fn known_plugin_ids_cover_every_reader() {
let source = include_str!("assemble.rs");
let mut readers: Vec<&str> = Vec::new();
for (index, _) in source.match_indices(".plugins") {
let rest = &source[index + ".plugins".len()..];
let Some(open) = rest.find(".get(") else {
continue;
};
if rest[..open].chars().any(|c| !c.is_whitespace()) {
continue;
}
let after = &rest[open + ".get(".len()..];
let after = after.trim_start();
let Some(quoted) = after.strip_prefix('"') else {
continue;
};
if let Some(end) = quoted.find('"') {
readers.push("ed[..end]);
}
}
assert!(
!readers.is_empty(),
"the scan found no readers at all — it has stopped testing"
);
for id in &readers {
assert!(
super::KNOWN_PLUGIN_IDS.contains(id),
"plugins.{id} is read but missing from KNOWN_PLUGIN_IDS, so the \
daemon would warn about a plugin it honours"
);
}
for id in super::KNOWN_PLUGIN_IDS {
assert!(
readers.contains(id),
"KNOWN_PLUGIN_IDS names {id}, which nothing reads"
);
}
}
#[test]
fn a_dead_sub_key_under_the_new_id_is_reported() {
let config = config_with_telemetry(serde_json::json!({
"otlp_endpoint": "http://legacy:4318"
}));
assert!(super::unknown_plugin_ids(&config).is_empty());
let warnings = super::ignored_config_warnings(&config);
assert!(
warnings
.iter()
.any(|w| w.contains("plugins.bitrouter-telemetry.otlp_endpoint")),
"got: {warnings:?}"
);
}
}
#[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_fusion_alias, 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 assembly_rejects_a_fusion_alias_that_hijacks_reserved_routing() {
let mut config = Config::default();
config.server_tools.fusion = Some(
bitrouter_sdk::language_model::server_tools::fusion::config::FusionSettings {
alias: Some("bitrouter/auto".to_string()),
outer_model: Some("anthropic/claude-opus-4.8".to_string()),
..Default::default()
},
);
let result = build_fusion_alias(&config);
let error = match result {
Ok(_) => panic!("reserved Fusion alias must fail app assembly"),
Err(error) => error,
};
assert!(error.to_string().contains("server_tools.fusion.alias"));
assert!(error.to_string().contains("bitrouter/auto"));
}
#[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");
}
}
#[cfg(test)]
mod trajectory_assembly_tests {
use std::collections::{BTreeMap, BTreeSet};
use bitrouter_sdk::caller::CallerContext;
use bitrouter_sdk::language_model::{
ApiProtocol, GenerationParams, Message, PipelineRequest, Prompt, Role,
};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::{Config, build_app_with_path};
use crate::eval::types::EvalScope;
use crate::policy_lock::{
CertificateSource, PolicyCertificate, PolicyDefinition, PolicyLock, PromotionVerdict,
RouteOwner, deterministic_yaml, semantic_digest,
};
use crate::trajectory::guard::{IncompleteHistoryAction, ProgressGuardPolicy};
use crate::workflow_state::predictive::compiled_predictor_contract;
async fn assemble_named_policy(
trajectory_enabled: bool,
progress_guard: Option<ProgressGuardPolicy>,
) -> anyhow::Result<(super::Assembled, String)> {
let upstream = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "chatcmpl-eval-compat",
"object": "chat.completion",
"model": "economy-model",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "complete"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 11, "completion_tokens": 7, "total_tokens": 18}
})))
.mount(&upstream)
.await;
let home = tempfile::tempdir()?;
let config_path = home.path().join("bitrouter.yaml");
let yaml = format!(
r#"
server:
skip_auth: true
database:
url: "sqlite::memory:"
trajectory:
enabled: {trajectory_enabled}
policy:
path: "./policy-lock.yaml"
mode: frozen
providers:
vendor:
api_base: "{upstream_uri}"
api_key: test-key
api_protocol:
- "*": chat_completions
models:
- id: economy-model
- id: strong-model
presets:
coding:
model: "vendor:strong-model"
policy: coding
"#,
upstream_uri = upstream.uri(),
);
let config = bitrouter_sdk::config::parse_with(&yaml, |_| None)?;
let guarded = progress_guard.is_some();
const ROUTE_KEY: &str = "agent_route/v1|unknown|unknown|normal";
const TEST_DIGEST: &str =
"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let policy = PolicyDefinition {
tiers: BTreeMap::from([
("economy".into(), "vendor:economy-model".into()),
("strong".into(), "vendor:strong-model".into()),
]),
routes: if guarded {
BTreeMap::new()
} else {
BTreeMap::from([(ROUTE_KEY.into(), "economy".into())])
},
default_tier: Some(if guarded { "economy" } else { "strong" }.into()),
tool_use_tier: Some("strong".into()),
tool_safe_tiers: vec!["strong".into()],
progress_guard,
predictor: (!guarded).then(compiled_predictor_contract),
..PolicyDefinition::default()
};
let mut lock = PolicyLock::default();
if !guarded {
lock.certificates.insert(
"coding".into(),
BTreeMap::from([(
ROUTE_KEY.into(),
PolicyCertificate {
owner: RouteOwner::Operator,
selected_tier: "economy".into(),
baseline_tier: Some("strong".into()),
source: CertificateSource::Operator,
eligible_episodes: 0,
independent_tasks: 0,
quality: None,
economics: None,
latency: None,
critical_violations: 0,
verdict: PromotionVerdict::Promote,
evaluator_config_digest: None,
compiler_config_digest: TEST_DIGEST.into(),
evidence_digest: TEST_DIGEST.into(),
legacy: None,
},
)]),
);
}
lock.policies.insert("coding".into(), policy);
let digest = semantic_digest(&lock)?;
tokio::fs::write(
home.path().join("policy-lock.yaml"),
deterministic_yaml(&lock)?,
)
.await?;
let assembled = build_app_with_path(&config, Some(&config_path)).await?;
let prompt = Prompt {
model: "@coding".into(),
system: None,
system_provider_metadata: Default::default(),
messages: vec![Message::text(Role::User, "generic request")],
tools: Vec::new(),
params: GenerationParams::default(),
response_format: None,
tool_choice: None,
stream: false,
};
let mut request = PipelineRequest::new("@coding", CallerContext::local(), prompt);
request.request_id = "request-eval-compat".into();
request.inbound_protocol = Some(ApiProtocol::ChatCompletions);
assembled
.app
.language_model()
.ok_or_else(|| anyhow::anyhow!("assembled App has no language model pipeline"))?
.execute(request)
.await?;
if let Some(publisher) = &assembled.trajectory_outbox_publisher {
publisher.wait_for_idle().await;
}
Ok((assembled, digest))
}
#[tokio::test]
async fn disabled_trajectory_preserves_named_policy_request_eval() -> anyhow::Result<()> {
let (assembled, digest) = assemble_named_policy(false, None).await?;
let subjects = assembled
.eval_service
.store()
.list_subjects_for_owner("local")
.await?;
assert_eq!(subjects.len(), 1);
let subject = &subjects[0];
assert_eq!(subject.scope, EvalScope::Request);
assert_eq!(subject.policy_digest, digest);
assert_eq!(subject.decisions.len(), 1);
assert_eq!(subject.decisions[0].policy, "coding");
assert_eq!(subject.decisions[0].selected_tier, "economy");
assert_eq!(
subject.decisions[0].baseline_tier.as_deref(),
Some("strong")
);
assert!(
assembled
.eval_service
.store()
.list_subjects_for_owner("other-owner")
.await?
.is_empty()
);
Ok(())
}
#[tokio::test]
async fn unguarded_policy_preserves_request_eval_when_trajectory_is_enabled()
-> anyhow::Result<()> {
let (assembled, digest) = assemble_named_policy(true, None).await?;
let subjects = assembled
.eval_service
.store()
.list_subjects_for_owner("local")
.await?;
assert_eq!(subjects.len(), 1);
assert_eq!(subjects[0].scope, EvalScope::Request);
assert_eq!(subjects[0].policy_digest, digest);
assert_eq!(subjects[0].decisions.len(), 1);
assert_eq!(subjects[0].decisions[0].policy, "coding");
assert_eq!(subjects[0].decisions[0].selected_tier, "economy");
assert_eq!(
subjects[0].decisions[0].baseline_tier.as_deref(),
Some("strong")
);
assert!(assembled.pending_eval_decisions.is_empty());
assert_eq!(assembled.response_observer.buffered_request_count(), 0);
Ok(())
}
#[tokio::test]
async fn guarded_policy_emits_only_episode_eval_without_pending_decision() -> anyhow::Result<()>
{
let guard = ProgressGuardPolicy {
escalation_tier: "strong".into(),
protected_tiers: BTreeSet::from(["strong".into()]),
max_consecutive_unprotected: Some(3),
max_same_projection_unprotected: None,
max_recovery_count: None,
max_episode_requests: None,
max_episode_elapsed_ms: None,
max_episode_cost_micro_usd: None,
hold_for_requests: 2,
incomplete_history: IncompleteHistoryAction::Observe,
};
let (assembled, digest) = assemble_named_policy(true, Some(guard)).await?;
let subjects = assembled
.eval_service
.store()
.list_subjects_for_owner("local")
.await?;
assert_eq!(subjects.len(), 1);
assert_eq!(subjects[0].scope, EvalScope::Episode);
assert_eq!(subjects[0].policy_digest, digest);
assert_eq!(subjects[0].decisions.len(), 1);
assert_eq!(subjects[0].decisions[0].policy, "coding");
assert_eq!(subjects[0].decisions[0].selected_tier, "economy");
assert_eq!(
subjects[0].decisions[0].baseline_tier.as_deref(),
Some("economy")
);
let observation = subjects[0]
.evidence
.iter()
.find(|evidence| evidence.kind == "routing.prediction_observation")
.ok_or_else(|| anyhow::anyhow!("guarded response observation evidence missing"))?;
assert_eq!(
observation
.attributes
.get("observed_action")
.map(String::as_str),
Some("answer_or_summarize")
);
assert!(assembled.pending_eval_decisions.is_empty());
assert_eq!(assembled.response_observer.buffered_request_count(), 0);
Ok(())
}
#[tokio::test]
async fn trajectory_activation_is_explicit_and_owns_the_existing_key_lifecycle()
-> anyhow::Result<()> {
let disabled_home = tempfile::tempdir()?;
let mut config = Config::default();
config.database.url = "sqlite::memory:".into();
let disabled =
build_app_with_path(&config, Some(&disabled_home.path().join("bitrouter.yaml")))
.await?;
assert!(!disabled.policy_runtime.trajectory_is_active());
assert!(disabled.trajectory_outbox_publisher.is_none());
assert!(!disabled_home.path().join("correlation.key").exists());
let enabled_home = tempfile::tempdir()?;
config.trajectory.enabled = true;
config.trajectory.outbox_batch_size = 7;
let enabled =
build_app_with_path(&config, Some(&enabled_home.path().join("bitrouter.yaml"))).await?;
assert!(enabled.policy_runtime.trajectory_is_active());
assert_eq!(
enabled
.trajectory_outbox_publisher
.as_ref()
.map(|publisher| publisher.configured_batch_size()),
Some(7)
);
assert!(enabled_home.path().join("correlation.key").is_file());
Ok(())
}
#[tokio::test]
async fn enabled_guarded_policy_persists_its_first_route_atomically() -> anyhow::Result<()> {
use std::collections::{BTreeMap, BTreeSet};
use bitrouter_sdk::caller::CallerContext;
use bitrouter_sdk::config::PresetConfig;
use bitrouter_sdk::language_model::{
ApiProtocol, GenerationParams, Message, ModelSelector, PipelineContext,
PipelineRequest, Prompt, Role,
};
use crate::policy_lock::{PolicyDefinition, PolicyLock, deterministic_yaml};
use crate::trajectory::correlation::CorrelatedRequest;
use crate::trajectory::guard::{IncompleteHistoryAction, ProgressGuardPolicy};
use crate::trajectory::store::TrajectoryStore;
let home = tempfile::tempdir()?;
let config_path = home.path().join("bitrouter.yaml");
let mut config = Config::default();
config.database.url = "sqlite::memory:".into();
config.trajectory.enabled = true;
config.presets.insert(
"coding".into(),
PresetConfig {
model: Some("vendor:strong".into()),
policy: Some("coding".into()),
..PresetConfig::default()
},
);
let mut lock = PolicyLock::default();
lock.policies.insert(
"coding".into(),
PolicyDefinition {
tiers: BTreeMap::from([
("economy".into(), "vendor:economy".into()),
("strong".into(), "vendor:strong".into()),
]),
default_tier: Some("economy".into()),
tool_use_tier: Some("strong".into()),
tool_safe_tiers: vec!["strong".into()],
progress_guard: Some(ProgressGuardPolicy {
escalation_tier: "strong".into(),
protected_tiers: BTreeSet::from(["strong".into()]),
max_consecutive_unprotected: Some(3),
max_same_projection_unprotected: None,
max_recovery_count: None,
max_episode_requests: Some(4),
max_episode_elapsed_ms: None,
max_episode_cost_micro_usd: None,
hold_for_requests: 2,
incomplete_history: IncompleteHistoryAction::Observe,
}),
..PolicyDefinition::default()
},
);
tokio::fs::write(
home.path().join("policy-lock.yaml"),
deterministic_yaml(&lock)?,
)
.await?;
let assembled = build_app_with_path(&config, Some(&config_path)).await?;
let prompt = Prompt {
model: "vendor:strong".into(),
system: None,
system_provider_metadata: Default::default(),
messages: vec![Message::text(Role::User, "generic request")],
tools: Vec::new(),
params: GenerationParams::default(),
response_format: None,
tool_choice: None,
stream: false,
};
let mut request = PipelineRequest::new(
"vendor:strong",
CallerContext::new("local-key", "local"),
prompt,
);
request.request_id = "request-first".into();
request.inbound_protocol = Some(ApiProtocol::ChatCompletions);
let mut context = PipelineContext::new(request);
assembled
.policy_runtime
.select_variant("coding", None, &mut context)
.await?;
let correlated = context
.extension::<CorrelatedRequest>()
.ok_or_else(|| anyhow::anyhow!("enabled guarded request was not correlated"))?;
let events = TrajectoryStore::new(assembled.db)
.events_for_episode("local", &correlated.episode_id)
.await?;
assert_eq!(events.len(), 2);
assert_eq!(
events[0].request_id.as_deref(),
Some(correlated.request_id.as_str())
);
assert_eq!(
events[1]
.evidence
.categorical
.get("route.policy")
.map(String::as_str),
Some("coding")
);
Ok(())
}
#[tokio::test]
async fn enabled_startup_applies_configured_retention_before_serving() -> anyhow::Result<()> {
use std::collections::BTreeMap;
use bitrouter_sdk::language_model::{ApiProtocol, GenerationParams, Message, Prompt, Role};
use crate::trajectory::canonical::{Canonicalizer, CorrelationKey};
use crate::trajectory::correlation::TrajectoryRuntime;
use crate::trajectory::store::TrajectoryStore;
use crate::trajectory::types::{
RequestStatus, Settlement, TRAJECTORY_SCHEMA_VERSION, TrajectoryEvent,
TrajectoryEventKind, TrajectoryEvidence,
};
let home = tempfile::tempdir()?;
let database_path = home.path().join("retention.db");
let database_url = format!("sqlite://{}?mode=rwc", database_path.display());
let db = crate::db::connect(&database_url).await?;
crate::db::run_migrations(&db).await?;
let runtime = TrajectoryRuntime::new(
TrajectoryStore::new(db.clone()),
Canonicalizer::new(CorrelationKey::from_bytes([51; 32])?),
);
let root = runtime
.begin_request(
"owner-retention",
"request-retention",
ApiProtocol::ChatCompletions,
&Prompt {
model: "inbound".into(),
system: None,
system_provider_metadata: BTreeMap::new(),
messages: vec![Message::text(Role::User, "expired root")],
tools: Vec::new(),
params: GenerationParams::default(),
response_format: None,
tool_choice: None,
stream: false,
},
"2000-01-01T00:00:00Z",
)
.await?;
let mut route = TrajectoryEvent {
schema_version: TRAJECTORY_SCHEMA_VERSION,
event_id: "route-retention".into(),
owner_user_id: "owner-retention".into(),
episode_id: root.episode_id.clone(),
request_id: Some(root.request_id.clone()),
sequence: 2,
kind: TrajectoryEventKind::RouteIntentRecorded,
evidence: TrajectoryEvidence {
structural: BTreeMap::new(),
categorical: BTreeMap::from([
(
"route.projection".into(),
"agent_trace/v2|opening|normal".into(),
),
("route.selected_tier".into(), "economy".into()),
("route.workflow_state".into(), "opening".into()),
]),
digests: BTreeMap::new(),
route_measurement: None,
},
captured_at: "2000-01-01T00:00:01Z".into(),
content_digest: String::new(),
};
route.content_digest = route.semantic_digest()?;
runtime
.store()
.append_route_intent("owner-retention", route)
.await?;
let mut terminal = TrajectoryEvent {
schema_version: TRAJECTORY_SCHEMA_VERSION,
event_id: "settle-retention".into(),
owner_user_id: "owner-retention".into(),
episode_id: root.episode_id.clone(),
request_id: Some(root.request_id.clone()),
sequence: 3,
kind: TrajectoryEventKind::RequestSettled,
evidence: TrajectoryEvidence {
structural: BTreeMap::new(),
categorical: BTreeMap::new(),
digests: BTreeMap::new(),
route_measurement: None,
},
captured_at: "2000-01-01T00:00:02Z".into(),
content_digest: String::new(),
};
terminal.content_digest = terminal.semantic_digest()?;
runtime
.store()
.settle_request(
"owner-retention",
Settlement {
event: terminal,
status: RequestStatus::Settled,
outbox: None,
},
)
.await?;
drop(runtime);
db.close().await?;
let mut config = Config::default();
config.database.url = database_url;
config.trajectory.enabled = true;
config.trajectory.retention_days = 30;
config.trajectory.outbox_batch_size = 1;
let assembled =
build_app_with_path(&config, Some(&home.path().join("bitrouter.yaml"))).await?;
assert!(
TrajectoryStore::new(assembled.db)
.resolve_episode_owner(&root.episode_id)
.await?
.is_none()
);
Ok(())
}
}