use std::path::Path;
use anyhow::{Result, bail};
use serde_json::Value;
use crate::features::cli::SetupArgs;
use crate::shared::api::managed::preflight_model;
use crate::shared::config::{AppConfig, ServerMode};
use crate::shared::i18n::Locale;
const REFUSED_KEYS: &[&str] = &["schema_version", "api_keys"];
pub fn apply_settings(
config: &mut AppConfig,
args: &SetupArgs,
loc: &'static Locale,
) -> Result<Vec<String>> {
let mut lines = Vec::new();
let mut wrote = |key: &str, value: &str, was: Option<String>| {
lines.push(match was {
Some(old) => loc.tf(
"setup.settings.line_was",
&[("key", key), ("value", value), ("was", &old)],
),
None => loc.tf("setup.settings.line", &[("key", key), ("value", value)]),
});
};
if let Some(model) = &args.model {
let path = absolute(model, loc)?;
preflight_model(&path, loc)?;
if config.engine.mode != ServerMode::Managed {
let was = mode_name(config.engine.mode);
config.engine.mode = ServerMode::Managed;
wrote("engine.mode", "managed", Some(was));
}
config.engine.managed.model_path = Some(path.clone());
wrote("engine.managed.model_path", &path, None);
}
if let Some(mmproj) = &args.mmproj {
let path = absolute(mmproj, loc)?;
if !Path::new(&path).is_file() {
bail!(
"{}",
loc.tf("ui.err.managed.mmproj_not_found", &[("path", &path)])
);
}
config.engine.managed.mmproj = Some(path.clone());
wrote("engine.managed.mmproj", &path, None);
}
if let Some(n) = args.ctx {
config.engine.managed.context_size = n;
wrote("engine.managed.context_size", &n.to_string(), None);
}
if let Some(n) = args.ngl {
config.engine.managed.gpu_layers = n;
wrote("engine.managed.gpu_layers", &n.to_string(), None);
}
if let Some(model) = &args.embed_model {
let path = absolute(model, loc)?;
preflight_model(&path, loc)?;
if config.embed.mode != ServerMode::Managed {
let was = mode_name(config.embed.mode);
config.embed.mode = ServerMode::Managed;
wrote("embed.mode", "managed", Some(was));
}
config.embed.managed.model_path = Some(path.clone());
wrote("embed.managed.model_path", &path, None);
}
for (key, raw) in &args.set {
let value = set_by_path(config, key, raw).map_err(|e| e.localized(key, raw, loc))?;
wrote(key, &render(&value), None);
}
Ok(lines)
}
pub fn clear_dead_binaries(config: &mut AppConfig) -> Vec<&'static str> {
let mut cleared = Vec::new();
let mut visit = |key: &'static str, field: &mut Option<String>| {
let dead = field.as_deref().map(str::trim).is_some_and(|p| {
let explicit = p.contains('/') || p.contains('\\');
explicit && !Path::new(p).is_file()
});
if dead {
*field = None;
cleared.push(key);
}
};
visit("engine.managed.binary", &mut config.engine.managed.binary);
visit(
"impersonation_engine.managed.binary",
&mut config.impersonation_engine.managed.binary,
);
visit("embed.managed.binary", &mut config.embed.managed.binary);
cleared
}
fn absolute(path: &Path, loc: &Locale) -> Result<String> {
match std::path::absolute(path) {
Ok(p) => Ok(p.display().to_string()),
Err(e) => bail!(
"{}",
loc.tf(
"setup.err.bad_path",
&[
("path", &path.display().to_string()),
("detail", &e.to_string())
],
)
),
}
}
fn mode_name(mode: ServerMode) -> String {
serde_json::to_value(mode)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_default()
}
fn render(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum SetError {
Refused,
UnknownKey,
WrongType(String),
}
impl SetError {
fn localized(&self, key: &str, raw: &str, loc: &Locale) -> anyhow::Error {
anyhow::anyhow!(
"{}",
match self {
SetError::Refused => loc.tf("setup.set.refused", &[("key", key)]),
SetError::UnknownKey => loc.tf("setup.set.unknown_key", &[("key", key)]),
SetError::WrongType(detail) => loc.tf(
"setup.set.wrong_type",
&[("key", key), ("value", raw), ("detail", detail)],
),
}
)
}
}
pub fn set_by_path(config: &mut AppConfig, key: &str, raw: &str) -> Result<Value, SetError> {
let segments: Vec<&str> = key.split('.').map(str::trim).collect();
if segments.iter().any(|s| s.is_empty()) {
return Err(SetError::UnknownKey);
}
if REFUSED_KEYS.contains(&segments[0]) {
return Err(SetError::Refused);
}
let literal =
serde_json::from_str::<Value>(raw).unwrap_or_else(|_| Value::String(raw.to_string()));
match try_set(config, &segments, &literal) {
Ok(next) => {
*config = next;
Ok(literal)
}
Err(SetError::WrongType(first)) if !literal.is_string() => {
let typed = Value::String(raw.to_string());
match try_set(config, &segments, &typed) {
Ok(next) => {
*config = next;
Ok(typed)
}
Err(_) => Err(SetError::WrongType(first)),
}
}
Err(e) => Err(e),
}
}
fn try_set(config: &AppConfig, segments: &[&str], value: &Value) -> Result<AppConfig, SetError> {
let mut doc = serde_json::to_value(config).map_err(|e| SetError::WrongType(e.to_string()))?;
let (last, parents) = segments.split_last().ok_or(SetError::UnknownKey)?;
let mut at = &mut doc;
for seg in parents {
let Value::Object(map) = at else {
return Err(SetError::UnknownKey);
};
at = map
.entry((*seg).to_string())
.or_insert_with(|| Value::Object(Default::default()));
}
let Value::Object(map) = at else {
return Err(SetError::UnknownKey);
};
map.insert((*last).to_string(), value.clone());
let next: AppConfig =
serde_json::from_value(doc).map_err(|e| SetError::WrongType(e.to_string()))?;
let back = serde_json::to_value(&next).map_err(|e| SetError::WrongType(e.to_string()))?;
let survived = segments
.iter()
.try_fold(&back, |at, seg| at.as_object()?.get(*seg));
if same(survived, value) {
Ok(next)
} else {
Err(SetError::UnknownKey)
}
}
fn same(got: Option<&Value>, want: &Value) -> bool {
match (got, want) {
(None, Value::Null) => true,
(None, Value::Array(a)) => a.is_empty(),
(None, _) => false,
(Some(Value::Number(a)), Value::Number(b)) => match (a.as_f64(), b.as_f64()) {
(Some(a), Some(b)) => (a - b).abs() <= 1e-6 * a.abs().max(b.abs()).max(1.0),
_ => a == b,
},
(Some(Value::Object(a)), Value::Object(b)) => b.iter().all(|(k, v)| same(a.get(k), v)),
(Some(Value::Array(a)), Value::Array(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| same(Some(x), y))
}
(Some(a), b) => a == b,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::i18n::{Lang, locale};
use std::path::PathBuf;
fn gguf(dir: &Path, name: &str) -> PathBuf {
let p = dir.join(name);
std::fs::write(&p, b"GGUF").unwrap();
p
}
#[test]
fn the_five_named_settings_land_and_a_changed_mode_is_said() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let model = gguf(dir.path(), "chat.gguf");
let mmproj = gguf(dir.path(), "mmproj.gguf");
let embed = gguf(dir.path(), "embed.gguf");
let mut config = AppConfig::default();
config.engine.mode = ServerMode::Claude; let args = SetupArgs {
model: Some(model.clone()),
mmproj: Some(mmproj.clone()),
embed_model: Some(embed.clone()),
ctx: Some(32768),
ngl: Some(40),
..Default::default()
};
let lines = apply_settings(&mut config, &args, loc).unwrap();
assert_eq!(config.engine.mode, ServerMode::Managed);
assert_eq!(config.embed.mode, ServerMode::Managed);
let m = &config.engine.managed;
assert_eq!(m.model_path.as_deref(), Some(model.to_str().unwrap()));
assert_eq!(m.mmproj.as_deref(), Some(mmproj.to_str().unwrap()));
assert_eq!((m.context_size, m.gpu_layers), (32768, 40));
assert_eq!(
config.embed.managed.model_path.as_deref(),
Some(embed.to_str().unwrap())
);
assert_eq!(m.binary, None);
let text = lines.join("\n");
assert!(text.contains("engine.mode = managed"), "{text}");
assert!(text.contains("claude"), "the old mode is named: {text}");
assert!(!text.contains("embed.mode"), "{text}");
assert!(
text.contains("engine.managed.context_size = 32768"),
"{text}"
);
}
#[test]
fn a_field_that_was_not_named_is_left_alone() {
let loc = locale(Lang::En);
let mut config = AppConfig::default();
config.engine.mode = ServerMode::External;
config.engine.managed.model_path = Some("/keep/me.gguf".into());
let before = config.clone();
let args = SetupArgs {
ctx: Some(65536),
..Default::default()
};
let lines = apply_settings(&mut config, &args, loc).unwrap();
assert_eq!(lines.len(), 1);
assert_eq!(config.engine.managed.context_size, 65536);
config.engine.managed.context_size = before.engine.managed.context_size;
assert_eq!(config, before, "only the context moved — not even the mode");
}
#[test]
fn a_relative_path_is_stored_absolute() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let model = gguf(dir.path(), "rel.gguf");
let cwd = std::env::current_dir().unwrap();
let given = model
.strip_prefix(&cwd)
.map(Path::to_path_buf)
.unwrap_or(model.clone());
let mut config = AppConfig::default();
let args = SetupArgs {
model: Some(given),
..Default::default()
};
apply_settings(&mut config, &args, loc).unwrap();
let stored = config.engine.managed.model_path.unwrap();
assert!(Path::new(&stored).is_absolute(), "{stored}");
}
#[test]
fn a_path_that_names_no_file_is_refused_in_the_launchs_own_words() {
let dir = tempfile::tempdir().unwrap();
let real = gguf(dir.path(), "real.gguf");
for lang in [Lang::En, Lang::Ru] {
let loc = locale(lang);
for args in [
SetupArgs {
model: Some(dir.path().join("nope.gguf")),
..Default::default()
},
SetupArgs {
embed_model: Some(dir.path().join("nope.gguf")),
..Default::default()
},
] {
let err = apply_settings(&mut AppConfig::default(), &args, loc).unwrap_err();
let path = dir.path().join("nope.gguf").display().to_string();
assert_eq!(
err.to_string(),
loc.tf("ui.err.managed.model_not_found", &[("path", &path)]),
"{lang:?}"
);
}
let args = SetupArgs {
model: Some(real.clone()),
mmproj: Some(dir.path().join("no-projector.gguf")),
..Default::default()
};
let err = apply_settings(&mut AppConfig::default(), &args, loc).unwrap_err();
assert!(err.to_string().contains("no-projector.gguf"), "{err}");
}
}
#[test]
fn a_split_model_must_be_named_by_its_first_part() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
gguf(dir.path(), "big-00001-of-00002.gguf");
let second = gguf(dir.path(), "big-00002-of-00002.gguf");
let args = SetupArgs {
model: Some(second),
..Default::default()
};
let err = apply_settings(&mut AppConfig::default(), &args, loc).unwrap_err();
assert!(err.to_string().contains("00001-of-00002"), "{err}");
}
#[test]
fn set_reaches_any_field_by_its_path_in_the_file() {
let mut c = AppConfig::default();
assert_eq!(
set_by_path(&mut c, "engine.managed.sessions", "4"),
Ok(Value::from(4))
);
assert_eq!(c.engine.managed.sessions, 4);
assert_eq!(
set_by_path(&mut c, "engine.managed.no_mmap", "true"),
Ok(Value::Bool(true))
);
assert!(c.engine.managed.no_mmap);
set_by_path(&mut c, "engine.managed.host", "0.0.0.0").unwrap();
assert_eq!(c.engine.managed.host, "0.0.0.0");
set_by_path(&mut c, "engine.mode", "external").unwrap();
assert_eq!(c.engine.mode, ServerMode::External);
set_by_path(&mut c, "engine.managed.flash_attn", "on").unwrap();
set_by_path(&mut c, "tools.python_enabled", "true").unwrap();
assert!(c.tools.python_enabled);
c.engine.managed.mmproj = Some("/x".into());
set_by_path(&mut c, "engine.managed.mmproj", "null").unwrap();
assert_eq!(c.engine.managed.mmproj, None);
set_by_path(&mut c, "default_sampling.temperature", "0.7").unwrap();
assert_eq!(c.default_sampling.temperature, Some(0.7));
}
#[test]
fn a_literal_falls_back_to_the_string_it_was_typed_as() {
let mut c = AppConfig::default();
assert_eq!(
set_by_path(&mut c, "engine.external.model_name", "4"),
Ok(Value::String("4".into()))
);
assert_eq!(c.engine.external.model_name.as_deref(), Some("4"));
let err = set_by_path(&mut c, "engine.managed.port", "eight-thousand").unwrap_err();
assert!(matches!(err, SetError::WrongType(_)), "{err:?}");
let err = set_by_path(&mut c, "engine.managed.port", "true").unwrap_err();
let SetError::WrongType(detail) = err else {
panic!("{err:?}")
};
assert!(detail.contains("boolean"), "{detail}");
}
#[test]
fn a_key_the_config_does_not_have_is_refused_not_dropped() {
let mut c = AppConfig::default();
let before = c.clone();
for key in [
"engine.managed.modle_path", "engine.manged.model_path",
"nonsense",
"engine.managed.port.deeper",
"engine..mode",
"",
] {
assert_eq!(
set_by_path(&mut c, key, "x"),
Err(SetError::UnknownKey),
"{key:?}"
);
}
assert_eq!(c, before, "a refused key changes nothing");
}
#[test]
fn the_migrations_version_and_the_key_store_are_not_settable() {
let mut c = AppConfig::default();
let before = c.clone();
assert_eq!(
set_by_path(&mut c, "schema_version", "1"),
Err(SetError::Refused)
);
assert_eq!(
set_by_path(&mut c, "api_keys", "[]"),
Err(SetError::Refused)
);
assert_eq!(c, before);
}
#[test]
fn set_runs_after_the_flags_and_its_refusals_name_the_key() {
let dir = tempfile::tempdir().unwrap();
let model = gguf(dir.path(), "m.gguf");
let loc = locale(Lang::En);
let mut config = AppConfig::default();
let args = SetupArgs {
model: Some(model),
ctx: Some(4096),
set: vec![
("engine.managed.context_size".into(), "8192".into()),
("engine.managed.sessions".into(), "2".into()),
],
..Default::default()
};
let lines = apply_settings(&mut config, &args, loc).unwrap();
assert_eq!(config.engine.managed.context_size, 8192);
assert!(
lines
.last()
.unwrap()
.contains("engine.managed.sessions = 2")
);
for lang in [Lang::En, Lang::Ru] {
let loc = locale(lang);
for (key, value) in [
("engine.managed.modle_path", "x"),
("api_keys", "[]"),
("engine.managed.port", "nope"),
] {
let args = SetupArgs {
set: vec![(key.into(), value.into())],
..Default::default()
};
let err = apply_settings(&mut AppConfig::default(), &args, loc).unwrap_err();
assert!(err.to_string().contains(key), "{lang:?} {key}: {err}");
assert!(!err.to_string().contains('{'), "{lang:?} {key}: {err}");
}
}
}
#[test]
fn only_a_binary_path_that_names_no_file_is_cleared() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("llama-server");
std::fs::write(&real, b"x").unwrap();
let mut c = AppConfig::default();
c.engine.managed.binary = Some(r"C:\llama\llama-server.exe".into());
c.impersonation_engine.managed.binary = Some(real.display().to_string());
c.embed.managed.binary = Some("llama-server".into());
assert_eq!(clear_dead_binaries(&mut c), ["engine.managed.binary"]);
assert_eq!(c.engine.managed.binary, None);
c.engine.managed.binary = Some("/opt/gone/llama-server".into());
assert_eq!(clear_dead_binaries(&mut c), ["engine.managed.binary"]);
assert_eq!(
c.impersonation_engine.managed.binary.as_deref(),
Some(real.to_str().unwrap())
);
assert_eq!(c.embed.managed.binary.as_deref(), Some("llama-server"));
assert!(clear_dead_binaries(&mut AppConfig::default()).is_empty());
}
}