use anyhow::{anyhow, bail, Result};
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
use super::Config;
use crate::cache_profile::{CacheCapability, CacheProfile, CacheRetention};
use serde_json::{json, Map, Value};
use crate::models::{ModelConfig, ModelsConfig};
pub(super) use crate::serde_utils::deserialize_usize;
#[derive(Clone, Debug, Default)]
struct ModelRefConfig {
profile: Option<String>,
url: String,
api_key: String,
model: String,
proxy: Option<String>,
extra_options: std::collections::BTreeMap<String, Value>,
cache_profile: Option<CacheProfile>,
}
impl ModelRefConfig {
fn resolve(
&self,
profiles: &std::collections::BTreeMap<String, ModelConfig>,
fallback: &ModelConfig,
) -> Result<ModelConfig> {
let local = ModelConfig {
url: self.url.clone(),
api_key: self.api_key.clone(),
model: self.model.clone(),
proxy: self.proxy.clone(),
extra_options: self.extra_options.clone(),
cache_profile: self.cache_profile.clone(),
};
let resolved = match self.profile.as_deref() {
Some(profile) => {
let base = profiles
.get(profile)
.ok_or_else(|| anyhow!("unknown model profile `{profile}`"))?;
local.merged_over(base)
}
None => local,
};
Ok(resolved.merged_over(fallback))
}
}
pub(super) fn parse_kdl_config(contents: &str) -> Result<Config> {
let doc = contents.parse::<KdlDocument>()?;
parse_kdl_document(&doc)
}
fn parse_kdl_document(doc: &KdlDocument) -> Result<Config> {
let defaults = Config::default();
let profiles = parse_model_profiles(doc)?;
let llm = match doc.get("llm") {
Some(node) => parse_model_ref_node(node)?.resolve(&profiles, &defaults.models.llm)?,
None => defaults.models.llm.clone(),
};
let mut embedders = Vec::new();
for node in doc
.nodes()
.iter()
.filter(|node| node.name().value() == "embedder")
{
let parsed = parse_model_ref_node(node)?.resolve(&profiles, &defaults.models.embedder)?;
embedders.push(parsed);
}
let embedder = if let Some(first) = embedders.first() {
first.clone()
} else {
defaults.models.embedder.clone()
};
let reranker = match doc.get("reranker") {
Some(node) => parse_model_ref_node(node)?.resolve(&profiles, &defaults.models.reranker)?,
None => defaults.models.reranker.clone(),
};
Ok(Config {
models: ModelsConfig {
llm,
embedder,
reranker,
embedders,
embed_dim: optional_usize_node(doc, "embed_dim")?.unwrap_or(defaults.models.embed_dim),
},
watermark_tokens: optional_usize_node(doc, "watermark_tokens")?
.unwrap_or(defaults.watermark_tokens),
history_window: optional_usize_node(doc, "history_window")?
.unwrap_or(defaults.history_window),
name: optional_string_node(doc, "name")?.unwrap_or(defaults.name),
})
}
fn parse_model_profiles(
doc: &KdlDocument,
) -> Result<std::collections::BTreeMap<String, ModelConfig>> {
let mut profiles = std::collections::BTreeMap::new();
for node in doc
.nodes()
.iter()
.filter(|node| node.name().value() == "model_profile")
{
let (name, profile) = parse_named_model_profile(node)?;
if profiles.insert(name.clone(), profile).is_some() {
bail!("duplicate model profile `{name}`");
}
}
Ok(profiles)
}
fn parse_named_model_profile(node: &KdlNode) -> Result<(String, ModelConfig)> {
ensure_known_model_fields(node, true, false)?;
let name = optional_string_field(node, "name")?
.ok_or_else(|| anyhow!("model_profile is missing `name`"))?;
let parsed = parse_model_fields(node, false)?;
Ok((
name,
ModelConfig {
url: parsed.url,
api_key: parsed.api_key,
model: parsed.model,
proxy: parsed.proxy,
extra_options: parsed.extra_options,
cache_profile: parsed.cache_profile,
},
))
}
fn parse_model_ref_node(node: &KdlNode) -> Result<ModelRefConfig> {
ensure_known_model_fields(node, false, true)?;
parse_model_fields(node, true)
}
fn parse_model_fields(node: &KdlNode, allow_profile: bool) -> Result<ModelRefConfig> {
Ok(ModelRefConfig {
profile: if allow_profile {
optional_string_field(node, "profile")?
} else {
None
},
url: optional_string_field(node, "url")?.unwrap_or_default(),
api_key: optional_string_field(node, "api_key")?.unwrap_or_default(),
model: optional_string_field(node, "model")?.unwrap_or_default(),
proxy: optional_string_field(node, "proxy")?,
extra_options: optional_object_field(node, "extra_options")?.unwrap_or_default(),
cache_profile: parse_cache_profile(node)?,
})
}
fn parse_cache_profile(node: &KdlNode) -> Result<Option<CacheProfile>> {
let capability_name = optional_string_field(node, "cache_profile")?;
let affinity = optional_string_field(node, "cache_affinity")?;
let Some(capability_name) = capability_name else {
if affinity.is_some() {
bail!(
"`cache_affinity` requires an explicit `cache_profile` in `{}`",
node.name().value()
);
}
return Ok(None);
};
let capability = CacheCapability::parse(&capability_name).ok_or_else(|| {
anyhow!(
"unknown cache profile `{capability_name}` in `{}`",
node.name().value()
)
})?;
if matches!(capability, CacheCapability::Unsupported)
&& affinity
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
{
bail!(
"unsupported cache profile cannot declare `cache_affinity` in `{}`",
node.name().value()
);
}
let retention = match optional_string_field(node, "cache_retention")? {
Some(value) => {
let retention = CacheRetention::parse(&value).ok_or_else(|| {
anyhow!(
"unknown cache retention `{value}` in `{}`",
node.name().value()
)
})?;
Some(retention)
}
None => None,
};
if let Some(retention) = retention {
if !capability.retention_values().contains(&retention) {
bail!(
"cache profile `{}` cannot express `cache_retention {}` in `{}`",
capability.name(),
retention.name(),
node.name().value()
);
}
}
Ok(Some(CacheProfile {
capability,
cache_affinity: affinity,
retention,
}))
}
fn ensure_known_model_fields(node: &KdlNode, allow_name: bool, allow_profile: bool) -> Result<()> {
let mut allowed = vec![
"url",
"api_key",
"model",
"extra_options",
"proxy",
"cache_profile",
"cache_affinity",
"cache_retention",
];
if allow_name {
allowed.push("name");
}
if allow_profile {
allowed.push("profile");
}
for entry in node.entries().iter().filter_map(KdlEntry::name) {
if !allowed.contains(&entry.value()) {
bail!(
"unknown model field `{}` in `{}`",
entry.value(),
node.name().value()
);
}
}
if let Some(children) = node.children() {
for child in children.nodes() {
if !allowed.contains(&child.name().value()) {
bail!(
"unknown model field `{}` in `{}`",
child.name().value(),
node.name().value()
);
}
}
}
Ok(())
}
fn optional_node<T>(
doc: &KdlDocument,
name: &str,
parse: fn(&KdlNode) -> Result<T>,
) -> Result<Option<T>> {
doc.get(name).map(parse).transpose()
}
macro_rules! optional_node_parser {
($name:ident -> $ty:ty, $parse:ident) => {
fn $name(doc: &KdlDocument, name: &str) -> Result<Option<$ty>> {
optional_node(doc, name, $parse)
}
};
}
optional_node_parser!(optional_string_node -> String, parse_string_scalar_node);
optional_node_parser!(optional_usize_node -> usize, parse_usize_scalar_node);
fn parse_string_scalar_node(node: &KdlNode) -> Result<String> {
match node_to_config_value(node)? {
Value::String(value) => Ok(value),
other => bail!(
"expected string for `{}`, got {other:?}",
node.name().value()
),
}
}
fn parse_usize_scalar_node(node: &KdlNode) -> Result<usize> {
match node_to_config_value(node)? {
Value::Number(value) => value
.as_u64()
.and_then(|value| usize::try_from(value).ok())
.ok_or_else(|| anyhow!("expected non-negative usize for `{}`", node.name().value())),
other => bail!(
"expected integer for `{}`, got {other:?}",
node.name().value()
),
}
}
fn optional_string_field(node: &KdlNode, name: &str) -> Result<Option<String>> {
match optional_field_value(node, name)? {
Some(Value::String(value)) => Ok(Some(value)),
Some(Value::Null) => Ok(None),
Some(other) => bail!(
"expected string for `{}.{name}`, got {other:?}",
node.name().value()
),
None => Ok(None),
}
}
fn optional_object_field(
node: &KdlNode,
name: &str,
) -> Result<Option<std::collections::BTreeMap<String, Value>>> {
match optional_field_value(node, name)? {
Some(Value::Object(value)) => Ok(Some(value.into_iter().collect())),
Some(Value::Null) => Ok(None),
Some(other) => bail!(
"expected object for `{}.{name}`, got {other:?}",
node.name().value()
),
None => Ok(None),
}
}
fn optional_field_value(node: &KdlNode, name: &str) -> Result<Option<Value>> {
let prop = node.get(name).map(config_value_from_kdl);
let child = child_node(node, name)
.map(node_to_config_value)
.transpose()?;
match (prop, child) {
(Some(_), Some(_)) => bail!("duplicate `{name}` field in `{}`", node.name().value()),
(Some(value), None) => Ok(Some(value)),
(None, Some(value)) => Ok(Some(value)),
(None, None) => Ok(None),
}
}
fn child_node<'a>(node: &'a KdlNode, name: &str) -> Option<&'a KdlNode> {
node.children().and_then(|children| children.get(name))
}
fn node_to_config_value(node: &KdlNode) -> Result<Value> {
let args = positional_entry_values(node);
let mut object: Map<String, Value> = node
.entries()
.iter()
.filter_map(|entry| {
entry.name().map(|name| {
(
name.value().to_string(),
config_value_from_kdl(entry.value()),
)
})
})
.collect();
if let Some(children) = node.children() {
if args.is_empty()
&& object.is_empty()
&& children
.nodes()
.iter()
.all(|child| child.name().value() == "-")
{
return children
.nodes()
.iter()
.map(node_to_config_value)
.collect::<Result<Vec<_>>>()
.map(Value::Array);
}
for child in children.nodes() {
insert_object_value(
&mut object,
child.name().value().to_string(),
node_to_config_value(child)?,
);
}
}
if !object.is_empty() {
if !args.is_empty() {
bail!(
"mixed positional args and object fields are not supported in `{}`",
node.name().value()
);
}
return Ok(Value::Object(object));
}
match args.len() {
0 => Ok(Value::Null),
1 => Ok(args.into_iter().next().unwrap()),
_ => Ok(Value::Array(args)),
}
}
fn positional_entry_values(node: &KdlNode) -> Vec<Value> {
node.entries()
.iter()
.filter(|entry| entry.name().is_none())
.map(|entry| config_value_from_kdl(entry.value()))
.collect()
}
fn insert_object_value(object: &mut Map<String, Value>, key: String, value: Value) {
match object.remove(&key) {
Some(Value::Array(mut values)) => {
values.push(value);
object.insert(key, Value::Array(values));
}
Some(existing) => {
object.insert(key, Value::Array(vec![existing, value]));
}
None => {
object.insert(key, value);
}
}
}
fn config_value_from_kdl(value: &KdlValue) -> Value {
match value {
KdlValue::Null => Value::Null,
KdlValue::Bool(value) => Value::Bool(*value),
KdlValue::String(value) => Value::String(value.clone()),
KdlValue::Integer(value) => json!(value),
KdlValue::Float(value) => json!(value),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn llm_of(kdl: &str) -> ModelConfig {
parse_kdl_config(kdl).expect("config parses").models.llm
}
#[test]
fn a_route_that_declares_no_cache_profile_gets_none() {
let llm = llm_of("llm {\n url \"https://example.invalid/v1\"\n model \"m\"\n}\n");
assert!(
llm.cache_profile.is_none(),
"absence stays absence: no field is sent"
);
}
#[test]
fn a_declared_retention_reaches_the_parsed_route() {
let llm = llm_of(
"llm {\n url \"https://example.invalid/v1\"\n model \"m\"\n cache_profile \"openai_prompt_cache_retention\"\n cache_affinity \"klbr\"\n cache_retention \"24h\"\n}\n",
);
let profile = llm.cache_profile.expect("profile");
assert_eq!(
profile.capability,
CacheCapability::OpenAiPromptCacheRetention
);
assert_eq!(profile.retention, Some(CacheRetention::OneDay));
assert_eq!(
profile
.native_request_fields()
.get("prompt_cache_retention"),
Some(&serde_json::json!("24h"))
);
}
#[test]
fn a_retention_the_declared_profile_cannot_express_is_refused_at_parse_time() {
let err = parse_kdl_config(
"llm {\n url \"https://example.invalid/v1\"\n model \"m\"\n cache_profile \"openai_prompt_cache_key\"\n cache_retention \"24h\"\n}\n",
)
.expect_err("a key-only route cannot express retention");
assert!(format!("{err:#}").contains("cannot express"), "{err:#}");
}
#[test]
fn an_unknown_retention_value_is_refused() {
let err = parse_kdl_config(
"llm {\n url \"https://example.invalid/v1\"\n model \"m\"\n cache_profile \"openai_prompt_cache_options\"\n cache_retention \"6h\"\n}\n",
)
.expect_err("unknown retention");
assert!(
format!("{err:#}").contains("unknown cache retention"),
"{err:#}"
);
}
#[test]
fn an_unknown_cache_field_is_refused_rather_than_ignored() {
let err = parse_kdl_config(
"llm {\n url \"https://example.invalid/v1\"\n model \"m\"\n cache_ttl \"24h\"\n}\n",
)
.expect_err("unknown field");
assert!(
format!("{err:#}").contains("unknown model field"),
"{err:#}"
);
}
}