use std::path::Path;
use anyhow::Context;
use serde_json::{Map, Value};
use crate::harness::{McpServer, McpTransport, RoutingOverlay, headers_map, v1_base_url};
#[derive(Debug, Clone, Copy)]
pub struct ConfigSynthesis {
pub dir: Option<&'static str>,
pub file: &'static str,
pub skeleton: &'static str,
pub models: Option<ModelList>,
pub default_model: Option<DefaultModel>,
pub mcp: McpShape,
pub env: &'static [(&'static str, &'static str)],
pub args: ArgSpec,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelList {
pub at: &'static str,
pub shape: ModelShape,
pub order: ModelOrder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelShape {
MapOfEmpty,
ArrayOfId,
ArrayOfProfile,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelOrder {
CatalogThenModel,
ModelThenCatalog,
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultModel {
pub at: &'static str,
pub format: DefaultFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefaultFormat {
Bare,
ProviderPrefixed,
}
#[derive(Debug, Clone, Copy)]
pub enum McpShape {
None,
NamedMap {
at: &'static str,
entry: McpEntryShape,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpEntryShape {
OpencodeTyped,
CommandArgsOrUrl,
}
#[derive(Debug, Clone, Copy)]
pub struct ArgSpec {
pub always: &'static [&'static str],
pub with_default_model: &'static [&'static str],
}
impl ConfigSynthesis {
pub fn render(
&self,
base_url: &str,
auth: &str,
model: Option<&str>,
catalog: &[String],
mcp: &[McpServer],
state_dir: &Path,
) -> anyhow::Result<RoutingOverlay> {
let dir = match self.dir {
Some(name) => state_dir.join(name),
None => state_dir.to_path_buf(),
};
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
let path = dir.join(self.file);
let mut config: Value =
serde_json::from_str(self.skeleton).context("parsing the harness config skeleton")?;
substitute_scalars(&mut config, &v1_base_url(base_url), auth);
let ids = self.model_ids(model, catalog);
if let Some(list) = &self.models {
set_pointer(&mut config, list.at, render_models(list.shape, &ids))?;
}
let default = ids_default(model, catalog);
if let (Some(spec), Some(default)) = (&self.default_model, default.as_deref()) {
set_pointer(
&mut config,
spec.at,
Value::String(spec.format.render(default)),
)?;
}
if let McpShape::NamedMap { at, entry } = self.mcp
&& !mcp.is_empty()
{
set_pointer(&mut config, at, render_mcp(entry, mcp))?;
}
std::fs::write(&path, serde_json::to_string_pretty(&config)?)
.with_context(|| format!("writing {}", path.display()))?;
let env = self
.env
.iter()
.map(|(name, value)| {
let rendered = value
.replace("{dir}", &dir.display().to_string())
.replace("{file}", &path.display().to_string())
.replace("{auth}", auth);
((*name).to_string(), rendered)
})
.collect();
let mut args: Vec<String> = self.args.always.iter().map(|a| (*a).to_string()).collect();
if let Some(default) = default {
args.extend(
self.args
.with_default_model
.iter()
.map(|a| a.replace("{default_model}", &default)),
);
}
Ok(RoutingOverlay { env, args })
}
fn model_ids(&self, model: Option<&str>, catalog: &[String]) -> Vec<String> {
let Some(list) = &self.models else {
return Vec::new();
};
match list.order {
ModelOrder::CatalogThenModel => {
let mut ids: Vec<String> = catalog.to_vec();
if let Some(m) = model
&& !ids.iter().any(|id| id == m)
{
ids.push(m.to_string());
}
ids
}
ModelOrder::ModelThenCatalog => model
.into_iter()
.map(str::to_string)
.chain(catalog.iter().cloned())
.collect(),
}
}
}
fn ids_default(model: Option<&str>, catalog: &[String]) -> Option<String> {
model
.map(str::to_string)
.or_else(|| catalog.first().cloned())
}
impl DefaultFormat {
fn render(self, id: &str) -> String {
match self {
Self::Bare => id.to_string(),
Self::ProviderPrefixed => format!("bitrouter/{id}"),
}
}
}
fn substitute_scalars(value: &mut Value, base_url_v1: &str, auth: &str) {
match value {
Value::String(text) => {
*text = text
.replace("{base_url_v1}", base_url_v1)
.replace("{auth}", auth);
}
Value::Array(items) => {
for item in items {
substitute_scalars(item, base_url_v1, auth);
}
}
Value::Object(map) => {
for (_, item) in map.iter_mut() {
substitute_scalars(item, base_url_v1, auth);
}
}
_ => {}
}
}
fn set_pointer(root: &mut Value, pointer: &str, value: Value) -> anyhow::Result<()> {
let segments: Vec<&str> = pointer
.strip_prefix('/')
.with_context(|| format!("config pointer '{pointer}' must start with '/'"))?
.split('/')
.collect();
let (last, parents) = segments
.split_last()
.with_context(|| format!("config pointer '{pointer}' is empty"))?;
let mut cursor = root;
for segment in parents {
let map = cursor
.as_object_mut()
.with_context(|| format!("config pointer '{pointer}' traverses a non-object"))?;
cursor = map
.entry((*segment).to_string())
.or_insert_with(|| Value::Object(Map::new()));
}
cursor
.as_object_mut()
.with_context(|| format!("config pointer '{pointer}' traverses a non-object"))?
.insert((*last).to_string(), value);
Ok(())
}
fn render_models(shape: ModelShape, ids: &[String]) -> Value {
match shape {
ModelShape::MapOfEmpty => {
let mut map = Map::new();
for id in ids {
map.entry(id.clone())
.or_insert_with(|| serde_json::json!({}));
}
Value::Object(map)
}
ModelShape::ArrayOfId => Value::Array(
ids.iter()
.map(|id| serde_json::json!({ "id": id }))
.collect(),
),
ModelShape::ArrayOfProfile => Value::Array(
ids.iter()
.map(|id| {
serde_json::json!({
"id": id,
"name": id,
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 200000,
"maxTokens": 8192,
})
})
.collect(),
),
}
}
fn render_mcp(entry: McpEntryShape, servers: &[McpServer]) -> Value {
let mut entries = Map::new();
for server in servers {
let rendered = match (entry, &server.transport) {
(McpEntryShape::OpencodeTyped, McpTransport::Stdio { command, args }) => {
let mut invocation = vec![command.clone()];
invocation.extend(args.iter().cloned());
serde_json::json!({ "type": "local", "command": invocation, "enabled": true })
}
(McpEntryShape::OpencodeTyped, McpTransport::Http { url, headers }) => {
serde_json::json!({
"type": "remote",
"url": url,
"enabled": true,
"headers": headers_map(headers),
})
}
(McpEntryShape::CommandArgsOrUrl, McpTransport::Stdio { command, args }) => {
serde_json::json!({ "command": command, "args": args })
}
(McpEntryShape::CommandArgsOrUrl, McpTransport::Http { url, headers }) => {
serde_json::json!({ "url": url, "headers": headers_map(headers) })
}
};
entries.insert(server.name.clone(), rendered);
}
Value::Object(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::harness::{self, McpServer, McpTransport};
const SYNTHESIZED: &[&str] = &["opencode", "pi-acp", "hermes-acp", "openclaw"];
fn mcp_stdio() -> McpServer {
McpServer {
name: "bitrouter_tools".to_string(),
transport: McpTransport::Stdio {
command: "bitrouter".to_string(),
args: vec!["mcp".to_string(), "serve".to_string()],
},
}
}
fn mcp_http() -> McpServer {
McpServer {
name: "bitrouter_skills".to_string(),
transport: McpTransport::Http {
url: "http://127.0.0.1:4356/mcp".to_string(),
headers: vec![("Authorization".to_string(), "Bearer brk_test".to_string())],
},
}
}
fn normalize(overlay: &RoutingOverlay, root: &Path) -> (Vec<(String, String)>, Vec<String>) {
let root = root.display().to_string();
let env = overlay
.env
.iter()
.map(|(name, value)| {
(
name.clone(),
value.replace(&root, "{root}").replace('\\', "/"),
)
})
.collect();
(env, overlay.args.clone())
}
#[test]
fn rendered_config_bytes_match_the_golden_fixtures() -> anyhow::Result<()> {
type Case = (
&'static str,
Vec<String>,
Option<&'static str>,
Vec<McpServer>,
);
let cases: [Case; 3] = [
("empty", Vec::new(), None, Vec::new()),
(
"pinned-outside-catalog",
vec!["a/one".to_string(), "b/two".to_string()],
Some("extra/model"),
Vec::new(),
),
(
"full",
vec!["a/one".to_string(), "b/two".to_string()],
Some("a/one"),
vec![mcp_stdio(), mcp_http()],
),
];
let update = std::env::var_os("UPDATE_CONFIG_SYNTHESIS_GOLDEN").is_some();
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/config_synthesis");
if update {
std::fs::create_dir_all(&dir)?;
}
for id in SYNTHESIZED {
let harness =
harness::by_id(id).ok_or_else(|| anyhow::anyhow!("{id} missing from catalog"))?;
for (case, catalog, model, mcp) in &cases {
let scratch = tempfile::tempdir()?;
let overlay = harness.launch_overlay(
"http://127.0.0.1:4356",
"brk_test",
*model,
catalog,
mcp,
scratch.path(),
)?;
let (env, args) = normalize(&overlay, scratch.path());
let spec = harness
.config_synthesis()
.ok_or_else(|| anyhow::anyhow!("{id} has no config synthesis"))?;
let written = spec
.dir
.map_or_else(
|| scratch.path().to_path_buf(),
|sub| scratch.path().join(sub),
)
.join(spec.file);
let body = std::fs::read_to_string(&written)?;
let actual = serde_json::to_string_pretty(&serde_json::json!({
"env": env.iter().map(|(k, v)| [k, v]).collect::<Vec<_>>(),
"args": args,
"file": spec.file,
"config": serde_json::from_str::<Value>(&body)?,
}))? + "\n";
let path = dir.join(format!("{id}-{case}.json"));
if update {
std::fs::write(&path, &actual)?;
continue;
}
let expected = std::fs::read_to_string(&path).with_context(|| {
format!(
"reading {} - regenerate with UPDATE_CONFIG_SYNTHESIS_GOLDEN=1",
path.display()
)
})?;
assert_eq!(
expected, actual,
"rendered config drifted for {id} ({case})"
);
}
}
assert!(
!update,
"fixtures rewritten; unset UPDATE_CONFIG_SYNTHESIS_GOLDEN to assert"
);
Ok(())
}
#[test]
fn rendered_configs_carry_the_catalog_in_each_container_shape() -> anyhow::Result<()> {
let catalog = vec!["a/one".to_string(), "b/two".to_string()];
let dir = tempfile::tempdir()?;
let body = |id: &str| -> anyhow::Result<Value> {
let spec = harness::by_id(id)
.and_then(harness::Harness::config_synthesis)
.ok_or_else(|| anyhow::anyhow!("{id}"))?;
let sub = dir.path().join(id);
spec.render(
"http://127.0.0.1:4356",
"brk_test",
None,
&catalog,
&[],
&sub,
)?;
let path = spec
.dir
.map_or_else(|| sub.clone(), |name| sub.join(name))
.join(spec.file);
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
};
let opencode = body("opencode")?;
assert!(opencode["provider"]["bitrouter"]["models"]["a/one"].is_object());
assert_eq!(opencode["model"], "bitrouter/a/one");
let pi = body("pi-acp")?;
assert_eq!(pi["providers"]["bitrouter"]["models"][0]["id"], "a/one");
let openclaw = body("openclaw")?;
assert_eq!(
openclaw["models"]["providers"]["bitrouter"]["models"][0]["contextWindow"],
200000
);
assert_eq!(openclaw["agents"]["defaults"]["model"], "bitrouter/a/one");
let hermes = body("hermes-acp")?;
assert_eq!(hermes["model"]["default"], "a/one");
assert!(hermes["model"].get("models").is_none());
Ok(())
}
}