feat(mvp-system): add toml config overlay and orchestrator cli args

- Layer TOML + env + CLI via config::TomlConfigOverlay
  (from_layers_with_path_and_args); resolve and validate VastAI config.
- Generate provider/cached-model/gguf/vastai CLI args for the orchestrator child; grow
  mvp_one_node_chat and worker_node.
- Adjust relay provisioning.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-07-08 13:14:49 +04:00
parent 6f1c048669
commit 1ab6264ca0
5 changed files with 1271 additions and 338 deletions

View file

@ -24,7 +24,6 @@ const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777";
const DEFAULT_NODE_IMAGE: &str = "swactor-mvp-node:latest";
const BASE_NODE_IMAGE: &str = "swactor-mvp-node-base:cuda12.6";
const MVP_RUNTIME_CONFIG_ENV: &str = "MVP_RUNTIME_CONFIG";
const CACHED_MODEL_HOST_ENV: &str = "MVP_CACHED_MODEL_HOST_PATH";
const DEFAULT_CACHED_MODEL_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf";
const REPO_MODEL_CACHE_DIR: &str = ".model-cache";
const DEFAULT_MAX_TOKENS: u32 = 64;
@ -138,8 +137,8 @@ impl Config {
I: IntoIterator<Item = String>,
{
let args = ParsedArgs::parse(provided_args)?;
let loaded = chat_config::Config::load(args.config_path.as_deref())?;
let file = loaded.config;
let loaded = chat_config::TomlConfigOverlay::load(args.config_path.as_deref())?;
let toml = loaded.overlay;
let profile = if args.vastai {
RuntimeConfigProfile::Deploy
} else {
@ -150,59 +149,59 @@ impl Config {
} else {
provider_from_env(profile)?
};
let relay_mode = relay_mode_from_sources(file.relay.mode.as_deref())?;
let relay_mode = relay_mode_from_sources(toml.relay.mode.as_deref())?;
let relay_url = first_non_empty([
env_optional("MVP_IROH_RELAY_URL"),
env_optional("SWACTOR_IROH_RELAY_URL"),
file.relay.url.clone(),
toml.relay.url.clone(),
]);
let node_image = first_non_empty([
args.image,
env_optional("MVP_NODE_IMAGE"),
if args.vastai {
file.vastai.image.clone()
toml.vastai.image.clone()
} else {
None
},
file.image.node.clone(),
toml.image.node.clone(),
Some(DEFAULT_NODE_IMAGE.to_owned()),
])
.expect("default image is non-empty");
let max_tokens = args
.max_tokens
.or(env_u32_optional("MVP_PROMPT_MAX_TOKENS")?)
.or(file.prompt.max_tokens)
.or(toml.prompt.max_tokens)
.unwrap_or(DEFAULT_MAX_TOKENS);
let dashboard = args
.dashboard
.or(env_bool_optional("MVP_DASHBOARD")?)
.or(file.prompt.dashboard)
.or(toml.prompt.dashboard)
.unwrap_or(true);
let build_image = args
.build_image
.or(env_bool_optional("MVP_BUILD_NODE_IMAGE")?)
.or(file.image.build)
.or(toml.image.build)
.unwrap_or(true);
let push_image = args
.push_image
.or(env_bool_optional("MVP_PUSH_NODE_IMAGE")?)
.or(file.image.push)
.or(toml.image.push)
.unwrap_or(false);
let force_image_refresh = args
.force_image_refresh
.or(env_bool_optional("MVP_FORCE_NODE_IMAGE_REFRESH")?)
.or(file.image.force_refresh)
.or(toml.image.force_refresh)
.unwrap_or(false);
let image_tag = first_non_empty([
args.image_tag,
env_optional("MVP_NODE_IMAGE_TAG"),
file.image.tag.clone(),
toml.image.tag.clone(),
]);
let rpc_addr = first_non_empty([
args.rpc_addr,
env_optional("MVP_PROMPT_RPC_ADDR"),
env_optional("MVP_PROMPT_RPC_BIND"),
file.prompt.rpc_addr.clone(),
toml.prompt.rpc_addr.clone(),
Some(DEFAULT_RPC_ADDR.to_owned()),
])
.expect("default RPC address is non-empty");
@ -211,23 +210,28 @@ impl Config {
return Err("--dump-logs cannot be combined with --datastream-frame-log; use one datastream log destination".to_owned());
}
(true, None) => Some(PathBuf::from("mvp-chat.log")),
(false, explicit) => {
explicit.or_else(|| env_optional("MVP_DATASTREAM_FRAME_LOG").map(PathBuf::from))
}
(false, explicit) => explicit
.or_else(|| env_optional("MVP_DATASTREAM_FRAME_LOG").map(PathBuf::from))
.or_else(|| toml.observability.datastream_frame_log.clone().map(PathBuf::from)),
};
let model_id = first_non_empty([env_optional("MVP_MODEL_ID"), file.model.id.clone()]);
let model_id = first_non_empty([env_optional("MVP_MODEL_ID"), toml.model.id.clone()]);
let gguf_repo =
first_non_empty([env_optional("MVP_GGUF_REPO"), file.model.gguf_repo.clone()]);
first_non_empty([env_optional("MVP_GGUF_REPO"), toml.model.gguf_repo.clone()]);
let gguf_file =
first_non_empty([env_optional("MVP_GGUF_FILE"), file.model.gguf_file.clone()]);
first_non_empty([env_optional("MVP_GGUF_FILE"), toml.model.gguf_file.clone()]);
let gguf_revision = first_non_empty([
env_optional("MVP_GGUF_REVISION"),
file.model.gguf_revision.clone(),
toml.model.gguf_revision.clone(),
]);
let max_context = env_u32_optional("MVP_MAX_CONTEXT")?.or(file.model.max_context);
let max_context = env_u32_optional("MVP_MAX_CONTEXT")?.or(toml.model.max_context);
let cached_model = match (args.cached_model, toml.docker.cached_model_host_path.clone()) {
(Some(cached_model), _) => Some(cached_model),
(None, Some(path)) => Some(CachedModelConfig::from_arg(Some(path))?),
(None, None) => None,
};
let vastai = if args.vastai {
Some(resolve_vastai_config(
&file.vastai,
&toml.vastai,
&node_image,
relay_url.clone(),
)?)
@ -250,7 +254,7 @@ impl Config {
image_tag,
push_image,
force_image_refresh,
cached_model: args.cached_model,
cached_model,
datastream_frame_log,
vastai_yes: args.vastai_yes,
model_id,
@ -261,6 +265,107 @@ impl Config {
vastai,
})
}
fn orchestrator_cli_args(&self, image_ref: &str) -> Vec<String> {
let mut args = vec![
"--runtime-config".to_owned(),
self.config_profile.as_str().to_owned(),
"--provider".to_owned(),
self.provider.as_str().to_owned(),
"--image".to_owned(),
image_ref.to_owned(),
"--rpc-bind".to_owned(),
self.rpc_addr.clone(),
"--max-tokens".to_owned(),
self.max_tokens.to_string(),
if self.dashboard {
"--dashboard".to_owned()
} else {
"--no-dashboard".to_owned()
},
];
if let Some(relay_url) = &self.relay_url {
args.extend(["--relay-url".to_owned(), relay_url.clone()]);
}
args.extend([
"--relay-mode".to_owned(),
relay_mode_env_value(&self.relay_mode).to_owned(),
]);
if let Some(model_id) = &self.model_id {
args.extend(["--model-id".to_owned(), model_id.clone()]);
}
if let Some(repo) = &self.gguf_repo {
args.extend(["--gguf-repo".to_owned(), repo.clone()]);
}
if let Some(file) = &self.gguf_file {
args.extend(["--gguf-file".to_owned(), file.clone()]);
}
if let Some(revision) = &self.gguf_revision {
args.extend(["--gguf-revision".to_owned(), revision.clone()]);
}
if let Some(max_context) = self.max_context {
args.extend(["--max-context".to_owned(), max_context.to_string()]);
}
if let Some(cached_model) = &self.cached_model {
args.extend([
"--cached-model-host-path".to_owned(),
cached_model.host_path.to_string_lossy().to_string(),
]);
}
if let Some(path) = &self.datastream_frame_log {
args.extend([
"--datastream-frame-log".to_owned(),
path.to_string_lossy().to_string(),
]);
}
if let Some(vastai) = &self.vastai {
args.extend([
"--vastai-api-key".to_owned(),
vastai.api_key.clone(),
"--vastai-bootstrap-command".to_owned(),
vastai.bootstrap_command.clone(),
"--no-vastai-confirm-lease".to_owned(),
]);
if let Some(disk_gb) = vastai.disk_gb {
args.extend(["--vastai-disk-gb".to_owned(), disk_gb.to_string()]);
}
if let Some(gpu_name) = &vastai.gpu_name {
args.extend(["--vastai-gpu-name".to_owned(), gpu_name.clone()]);
}
if let Some(min_gpu_ram_mb) = vastai.min_gpu_ram_mb {
args.extend([
"--vastai-min-gpu-ram-mb".to_owned(),
min_gpu_ram_mb.to_string(),
]);
}
if let Some(min_down_mbps) = vastai.min_down_mbps {
args.extend(["--vastai-min-down-mbps".to_owned(), min_down_mbps.to_string()]);
}
if let Some(min_up_mbps) = vastai.min_up_mbps {
args.extend(["--vastai-min-up-mbps".to_owned(), min_up_mbps.to_string()]);
}
if let Some(min_reliability) = vastai.min_reliability {
args.extend([
"--vastai-min-reliability".to_owned(),
min_reliability.to_string(),
]);
}
if let Some(require_verified) = vastai.require_verified {
args.push(if require_verified {
"--vastai-require-verified".to_owned()
} else {
"--no-vastai-require-verified".to_owned()
});
}
if let Some(onstart) = &vastai.onstart {
args.extend(["--vastai-onstart".to_owned(), onstart.clone()]);
}
if let Some(ssh_identity) = &vastai.ssh_identity {
args.extend(["--vastai-ssh-identity".to_owned(), ssh_identity.clone()]);
}
}
args
}
}
#[derive(Default)]
@ -660,82 +765,13 @@ struct OrchChild {
impl OrchChild {
fn spawn(config: &Config, image_ref: &str) -> Result<Self, String> {
let mut command = Command::new(&config.orch_bin);
let mut orch_args = config.orchestrator_cli_args(image_ref);
orch_args.extend(config.orch_args.clone());
command
.args(&config.orch_args)
.env("MVP_NODE_PROVIDER", config.provider.as_str())
.env(MVP_RUNTIME_CONFIG_ENV, config.config_profile.as_str())
.env("MVP_NODE_IMAGE", image_ref)
.env(
"MVP_IROH_RELAY_MODE",
relay_mode_env_value(&config.relay_mode),
)
.env("MVP_PROMPT_RPC_BIND", &config.rpc_addr)
.env("MVP_PROMPT_RPC_ADDR", &config.rpc_addr)
.env("MVP_PROMPT_MAX_TOKENS", config.max_tokens.to_string())
.env("MVP_DASHBOARD", if config.dashboard { "1" } else { "0" })
.args(&orch_args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null());
if let Some(relay_url) = &config.relay_url {
command.env("MVP_IROH_RELAY_URL", relay_url);
}
if let Some(vastai) = &config.vastai {
command
.env("MVP_VASTAI_API_KEY", &vastai.api_key)
.env("MVP_VASTAI_BOOTSTRAP_COMMAND", &vastai.bootstrap_command)
.env("MVP_VASTAI_CONFIRM_LEASE", "0");
if let Some(disk_gb) = vastai.disk_gb {
command.env("MVP_VASTAI_DISK_GB", disk_gb.to_string());
}
if let Some(gpu_name) = &vastai.gpu_name {
command.env("MVP_VASTAI_GPU_NAME", gpu_name);
}
if let Some(min_gpu_ram_mb) = vastai.min_gpu_ram_mb {
command.env("MVP_VASTAI_MIN_GPU_RAM_MB", min_gpu_ram_mb.to_string());
}
if let Some(min_down_mbps) = vastai.min_down_mbps {
command.env("MVP_VASTAI_MIN_DOWN_MBPS", min_down_mbps.to_string());
}
if let Some(min_up_mbps) = vastai.min_up_mbps {
command.env("MVP_VASTAI_MIN_UP_MBPS", min_up_mbps.to_string());
}
if let Some(min_reliability) = vastai.min_reliability {
command.env("MVP_VASTAI_MIN_RELIABILITY", min_reliability.to_string());
}
if let Some(require_verified) = vastai.require_verified {
command.env(
"MVP_VASTAI_REQUIRE_VERIFIED",
if require_verified { "1" } else { "0" },
);
}
if let Some(onstart) = &vastai.onstart {
command.env("MVP_VASTAI_ONSTART", onstart);
}
if let Some(ssh_identity) = &vastai.ssh_identity {
command.env("MVP_VASTAI_SSH_IDENTITY", ssh_identity);
}
}
if let Some(model_id) = &config.model_id {
command.env("MVP_MODEL_ID", model_id);
}
if let Some(repo) = &config.gguf_repo {
command.env("MVP_GGUF_REPO", repo);
}
if let Some(file) = &config.gguf_file {
command.env("MVP_GGUF_FILE", file);
}
if let Some(revision) = &config.gguf_revision {
command.env("MVP_GGUF_REVISION", revision);
}
if let Some(max_context) = config.max_context {
command.env("MVP_MAX_CONTEXT", max_context.to_string());
}
if let Some(cached_model) = &config.cached_model {
command.env(CACHED_MODEL_HOST_ENV, &cached_model.host_path);
}
if let Some(path) = &config.datastream_frame_log {
command.env("MVP_DATASTREAM_FRAME_LOG", path);
}
#[cfg(target_os = "linux")]
unsafe {
command.pre_exec(|| {
@ -1484,6 +1520,136 @@ mod tests {
}
}
fn assert_arg_value(args: &[String], flag: &str, expected: &str) {
let flag_index = args
.iter()
.position(|arg| arg == flag)
.unwrap_or_else(|| panic!("missing CLI flag {flag}; args={args:?}"));
assert_eq!(
args.get(flag_index + 1).map(String::as_str),
Some(expected),
"unexpected value for CLI flag {flag}; args={args:?}"
);
}
fn assert_flag(args: &[String], flag: &str) {
assert!(
args.iter().any(|arg| arg == flag),
"missing CLI flag {flag}; args={args:?}"
);
}
#[test]
fn orchestrator_cli_args_cover_wrapper_launch_config() {
let config = Config {
orch_bin: PathBuf::from("mvp-orchestrator"),
orch_args: Vec::new(),
rpc_addr: "127.0.0.1:20123".to_owned(),
node_image: "docker.io/example/config-node:ignored".to_owned(),
config_profile: RuntimeConfigProfile::Local,
provider: ProviderKind::Docker,
relay_mode: iroh::RelayMode::Default,
relay_url: Some("https://relay.example.com".to_owned()),
max_tokens: 37,
dashboard: false,
build_image: false,
image_tag: None,
push_image: false,
force_image_refresh: false,
cached_model: Some(CachedModelConfig {
host_path: PathBuf::from("/var/cache/swactor/model.gguf"),
display_path: PathBuf::from("model.gguf"),
}),
datastream_frame_log: Some(PathBuf::from("/tmp/mvp-chat-frames.jsonl")),
vastai_yes: false,
vastai: None,
model_id: Some("wrapper-model".to_owned()),
gguf_repo: Some("example/wrapper-repo".to_owned()),
gguf_file: Some("wrapper-model.gguf".to_owned()),
gguf_revision: None,
max_context: Some(768),
};
let args = config.orchestrator_cli_args("docker.io/example/prepared-node:latest");
assert_arg_value(&args, "--runtime-config", "local");
assert_arg_value(&args, "--provider", "docker");
assert_arg_value(&args, "--image", "docker.io/example/prepared-node:latest");
assert_arg_value(&args, "--rpc-bind", "127.0.0.1:20123");
assert_arg_value(&args, "--max-tokens", "37");
assert_flag(&args, "--no-dashboard");
assert_arg_value(&args, "--relay-url", "https://relay.example.com");
assert_arg_value(&args, "--model-id", "wrapper-model");
assert_arg_value(&args, "--gguf-repo", "example/wrapper-repo");
assert_arg_value(&args, "--gguf-file", "wrapper-model.gguf");
assert_arg_value(&args, "--max-context", "768");
assert_arg_value(
&args,
"--cached-model-host-path",
"/var/cache/swactor/model.gguf",
);
assert_arg_value(
&args,
"--datastream-frame-log",
"/tmp/mvp-chat-frames.jsonl",
);
}
#[test]
fn orchestrator_cli_args_cover_vastai_config() {
let mut vastai = valid_vastai_config();
vastai.onstart = Some("echo preparing vastai node".to_owned());
let config = Config {
orch_bin: PathBuf::from("mvp-orchestrator"),
orch_args: Vec::new(),
rpc_addr: DEFAULT_RPC_ADDR.to_owned(),
node_image: "ghcr.io/swactor/mvp-node:latest".to_owned(),
config_profile: RuntimeConfigProfile::Deploy,
provider: ProviderKind::VastAi,
relay_mode: iroh::RelayMode::Default,
relay_url: Some(vastai.relay_url.clone()),
max_tokens: 128,
dashboard: false,
build_image: false,
image_tag: Some("trial".to_owned()),
push_image: true,
force_image_refresh: false,
cached_model: None,
datastream_frame_log: None,
vastai_yes: true,
vastai: Some(vastai),
model_id: None,
gguf_repo: None,
gguf_file: None,
gguf_revision: None,
max_context: None,
};
let args = config.orchestrator_cli_args("ghcr.io/swactor/mvp-node:latest");
assert_arg_value(&args, "--vastai-api-key", "vast-key");
assert_arg_value(
&args,
"--vastai-bootstrap-command",
"/usr/local/bin/mvp-node",
);
assert_flag(&args, "--no-vastai-confirm-lease");
assert_arg_value(&args, "--vastai-disk-gb", "80");
assert_arg_value(&args, "--vastai-gpu-name", "RTX 4090");
assert_arg_value(&args, "--vastai-min-gpu-ram-mb", "16000");
assert_arg_value(&args, "--vastai-min-down-mbps", "100");
assert_arg_value(&args, "--vastai-min-up-mbps", "25");
assert_arg_value(&args, "--vastai-min-reliability", "0.98");
assert_flag(&args, "--vastai-require-verified");
assert_arg_value(&args, "--vastai-onstart", "echo preparing vastai node");
assert_arg_value(
&args,
"--vastai-ssh-identity",
"~/.ssh/swactor_vastai_ed25519",
);
}
#[test]
fn approval_parser_accepts_only_y_or_yes() {
for (input, expected) in [

File diff suppressed because it is too large Load diff

View file

@ -417,18 +417,19 @@ fn run() -> Result<(), String> {
"ready",
json!({"actor":orchestrator,"source":orchestrator_source}),
)?;
let node_actor = match stack.runtime.spawn(NodeAgentActor::new(
let node_agent = NodeAgentActor::new(
stage::NodeId(config.logical_node_id),
orchestrator,
Some(*reports.addr()),
)) {
);
let node_actor = match stack.runtime.spawn(node_agent) {
Ok(actor) => {
emit_stdio_node_event(
&config,
NODE_BOOTSTRAP_CHANNEL,
"node_agent",
"ready",
json!({"node_actor":actor}),
json!({"node_actor":actor,"source":"generated"}),
)?;
actor
}
@ -438,7 +439,7 @@ fn run() -> Result<(), String> {
NODE_BOOTSTRAP_CHANNEL,
"node_agent",
"failed",
json!({"error":error.to_string()}),
json!({"error":error.to_string(),"source":"generated"}),
)?;
return Err(format!("spawn node agent: {error}"));
}
@ -569,7 +570,7 @@ fn run() -> Result<(), String> {
.map_err(|e| format!("flush ready line: {e}"))?;
if let Some(prompt) = &config.self_test_prompt {
run_self_test(&mut worker, &config, prompt, &mut datastream)?;
run_self_test(&mut worker, &config, prompt, &mut datastream, &mut driver, &stack)?;
}
let shutdown_rx = spawn_stdin_shutdown_listener();
@ -1197,6 +1198,8 @@ fn run_self_test(
config: &DeploymentConfig,
prompt: &str,
datastream: &mut DatastreamEmitter,
driver: &mut IrohDriver,
stack: &DistributionRuntimeStack,
) -> Result<(), String> {
emit_node_event(
datastream,
@ -1206,7 +1209,7 @@ fn run_self_test(
"started",
json!({"prompt_bytes":prompt.len()}),
);
let mut pump = || {};
let mut pump = || pump_network(driver, stack);
worker.configure_role(
config.run_id,
config.stage_index,
@ -1664,6 +1667,7 @@ impl Drop for TinygradWorker {
}
}
fn spawn_stdin_shutdown_listener() -> Receiver<()> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {

View file

@ -4,18 +4,40 @@ use std::path::{Path, PathBuf};
use serde::Deserialize;
use swactor_vastai::SelectionPolicy;
const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
pub const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
/// Transient serde target for `.config/config.toml`.
/// This is not runtime state; it exists only to interpret TOML fields and overlay them onto hardcoded defaults.
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct Config {
pub struct TomlConfigOverlay {
pub runtime: RuntimeConfigOverlay,
pub provider: ProviderConfigOverlay,
pub image: ImageConfig,
pub relay: RelayConfig,
pub prompt: PromptConfig,
pub model: ModelConfig,
pub docker: DockerConfigOverlay,
pub observability: ObservabilityConfigOverlay,
pub vastai: VastAiConfig,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct RuntimeConfigOverlay {
pub profile: Option<String>,
pub run_id: Option<u64>,
pub node_id: Option<u64>,
pub stage_index: Option<u32>,
pub layer_end_exclusive: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct ProviderConfigOverlay {
pub kind: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct ImageConfig {
@ -45,12 +67,27 @@ pub struct PromptConfig {
#[serde(default)]
pub struct ModelConfig {
pub id: Option<String>,
pub gguf_local_path: Option<String>,
pub gguf_repo: Option<String>,
pub gguf_file: Option<String>,
pub gguf_revision: Option<String>,
pub tokenizer_local_path: Option<String>,
pub max_context: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct DockerConfigOverlay {
pub gpus: Option<String>,
pub cached_model_host_path: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct ObservabilityConfigOverlay {
pub datastream_frame_log: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[serde(default)]
pub struct VastAiConfig {
@ -58,20 +95,23 @@ pub struct VastAiConfig {
pub image: Option<String>,
pub bootstrap_command: Option<String>,
pub disk_gb: Option<u32>,
pub ssh_user: Option<String>,
pub confirm_lease: Option<bool>,
pub gpu_name: Option<String>,
pub min_gpu_ram_mb: Option<u64>,
pub min_down_mbps: Option<f64>,
pub min_up_mbps: Option<f64>,
pub min_reliability: Option<f64>,
pub require_verified: Option<bool>,
pub poll_interval_secs: Option<u64>,
pub onstart: Option<String>,
pub ssh_identity: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LoadedConfig {
pub struct LoadedTomlConfigOverlay {
pub path: Option<PathBuf>,
pub config: Config,
pub overlay: TomlConfigOverlay,
}
#[derive(Clone, Debug, PartialEq)]
@ -91,27 +131,28 @@ pub struct ResolvedVastAiConfig {
pub ssh_identity: Option<String>,
}
impl Config {
pub fn load(path: Option<&Path>) -> Result<LoadedConfig, String> {
impl TomlConfigOverlay {
pub fn load(path: Option<&Path>) -> Result<LoadedTomlConfigOverlay, String> {
match path {
Some(path) => Self::load_required(path).map(|config| LoadedConfig {
Some(path) => Self::load_required(path).map(|overlay| LoadedTomlConfigOverlay {
path: Some(path.to_path_buf()),
config,
overlay,
}),
None => {
let default = Path::new(DEFAULT_CONFIG_PATH);
if default.is_file() {
Self::load_required(default).map(|config| LoadedConfig {
path: Some(default.to_path_buf()),
config,
Self::load_optional(default).map(|overlay| LoadedTomlConfigOverlay {
path: overlay.as_ref().map(|_| default.to_path_buf()),
overlay: overlay.unwrap_or_default(),
})
}
}
}
pub fn load_optional(path: &Path) -> Result<Option<Self>, String> {
if path.is_file() {
Self::load_required(path).map(Some)
} else {
Ok(LoadedConfig {
path: None,
config: Self::default(),
})
}
}
Ok(None)
}
}
@ -222,8 +263,19 @@ mod tests {
#[test]
fn config_toml_parses_explicit_chat_and_vastai_fields() {
let config = Config::from_str(
let config = TomlConfigOverlay::from_str(
r#"
[runtime]
profile = "deploy"
run_id = 42
node_id = 7
stage_index = 2
layer_end_exclusive = 24
[provider]
kind = "vastai"
[image]
node = "ghcr.io/swactor/mvp-node:latest"
tag = "trial"
@ -243,15 +295,27 @@ dashboard = false
[model]
id = "smollm2-135m-instruct-q4"
gguf_local_path = "/models/local.gguf"
gguf_repo = "QuantFactory/SmolLM2-135M-Instruct-GGUF"
gguf_file = "SmolLM2-135M-Instruct.Q4_K_M.gguf"
gguf_revision = "main"
tokenizer_local_path = "/models/tokenizer.json"
max_context = 512
[docker]
gpus = "all"
cached_model_host_path = "/cache/model.gguf"
[observability]
datastream_frame_log = "/tmp/frames.jsonl"
[vastai]
api_key = "vast-key"
image = "registry.example.com/team/mvp-node:latest"
bootstrap_command = "/opt/mvp/node --join"
disk_gb = 80
ssh_user = "ubuntu"
confirm_lease = true
gpu_name = "RTX 4090"
min_gpu_ram_mb = 24000
min_down_mbps = 250.5
@ -260,10 +324,17 @@ min_reliability = 0.99
require_verified = true
onstart = "echo preparing"
ssh_identity = "~/.ssh/swactor_vastai_ed25519"
poll_interval_secs = 30
"#,
)
.expect("explicit config TOML parses");
assert_eq!(config.runtime.profile.as_deref(), Some("deploy"));
assert_eq!(config.runtime.run_id, Some(42));
assert_eq!(config.runtime.node_id, Some(7));
assert_eq!(config.runtime.stage_index, Some(2));
assert_eq!(config.runtime.layer_end_exclusive, Some(24));
assert_eq!(config.provider.kind.as_deref(), Some("vastai"));
assert_eq!(
config.image.node.as_deref(),
Some("ghcr.io/swactor/mvp-node:latest")
@ -281,6 +352,10 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
assert_eq!(config.prompt.max_tokens, Some(128));
assert_eq!(config.prompt.dashboard, Some(false));
assert_eq!(config.model.id.as_deref(), Some("smollm2-135m-instruct-q4"));
assert_eq!(
config.model.gguf_local_path.as_deref(),
Some("/models/local.gguf")
);
assert_eq!(
config.model.gguf_repo.as_deref(),
Some("QuantFactory/SmolLM2-135M-Instruct-GGUF")
@ -290,7 +365,20 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
Some("SmolLM2-135M-Instruct.Q4_K_M.gguf")
);
assert_eq!(config.model.gguf_revision.as_deref(), Some("main"));
assert_eq!(
config.model.tokenizer_local_path.as_deref(),
Some("/models/tokenizer.json")
);
assert_eq!(config.model.max_context, Some(512));
assert_eq!(config.docker.gpus.as_deref(), Some("all"));
assert_eq!(
config.docker.cached_model_host_path.as_deref(),
Some("/cache/model.gguf")
);
assert_eq!(
config.observability.datastream_frame_log.as_deref(),
Some("/tmp/frames.jsonl")
);
assert_eq!(config.vastai.api_key.as_deref(), Some("vast-key"));
assert_eq!(
config.vastai.image.as_deref(),
@ -301,12 +389,15 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
Some("/opt/mvp/node --join")
);
assert_eq!(config.vastai.disk_gb, Some(80));
assert_eq!(config.vastai.ssh_user.as_deref(), Some("ubuntu"));
assert_eq!(config.vastai.confirm_lease, Some(true));
assert_eq!(config.vastai.gpu_name.as_deref(), Some("RTX 4090"));
assert_eq!(config.vastai.min_gpu_ram_mb, Some(24_000));
assert_eq!(config.vastai.min_down_mbps, Some(250.5));
assert_eq!(config.vastai.min_up_mbps, Some(50.25));
assert_eq!(config.vastai.min_reliability, Some(0.99));
assert_eq!(config.vastai.require_verified, Some(true));
assert_eq!(config.vastai.poll_interval_secs, Some(30));
assert_eq!(config.vastai.onstart.as_deref(), Some("echo preparing"));
assert_eq!(
config.vastai.ssh_identity.as_deref(),
@ -323,7 +414,7 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
));
let _ = fs::remove_file(&path);
let error = Config::load(Some(&path)).expect_err("missing explicit config path errors");
let error = TomlConfigOverlay::load(Some(&path)).expect_err("missing explicit config path errors");
assert!(
error.contains(&path.display().to_string()),

View file

@ -126,12 +126,22 @@ impl RelayProvider for StaticRelayProvider {
}
pub fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> {
match relay_mode_setting_from_env().as_deref() {
let mode = relay_mode_setting_from_env();
let url = selected_relay_url_from_env();
relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref())
}
pub fn relay_runtime_config_from_settings(
run_id: u64,
mode: Option<&str>,
url: Option<&str>,
) -> Result<RelayRuntimeConfig, String> {
match mode {
Some("disabled") => Ok(RelayRuntimeConfig {
mode: RelayMode::Disabled,
url: None,
}),
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id),
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url),
Some(other) => Err(format!(
"unsupported {MVP_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
)),
@ -151,13 +161,15 @@ pub fn selected_relay_url_from_env() -> Option<String> {
fn relay_runtime_config_from_optional_static_provider(
run_id: u64,
url: Option<&str>,
) -> Result<RelayRuntimeConfig, String> {
let Some(mut provider) = StaticRelayProvider::from_env()? else {
let Some(raw_url) = url.map(str::trim).filter(|url| !url.is_empty()) else {
return Ok(RelayRuntimeConfig {
mode: RelayMode::Default,
url: None,
});
};
let mut provider = StaticRelayProvider::from_url_str(raw_url)?;
let lease = provider.provision_relay(RelayProvisionRequest {
run_id,
purpose: RelayPurpose::Combined,