Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46c11b9b88 |
Generated
+1
-1
@@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "cortiq-gateway"
|
||||
version = "0.2.19"
|
||||
version = "0.2.20"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cortiq-gateway"
|
||||
version = "0.2.19"
|
||||
version = "0.2.20"
|
||||
edition = "2021"
|
||||
rust-version = "1.75"
|
||||
license = "Apache-2.0"
|
||||
|
||||
+28
-12
@@ -908,28 +908,44 @@ async fn put_settings(State(state): State<SharedState>, Json(b): Json<SettingsBo
|
||||
needs_restart = true;
|
||||
cfg.cache = v;
|
||||
}
|
||||
let mut cmf_apply: Option<(Vec<crate::config::CmfServer>, CmfCfg)> = None;
|
||||
if let Some(v) = b.cmf {
|
||||
// manage_server / local_model / threads / gpu apply on restart (the server
|
||||
// is spawned once at startup with those env vars); local_only / router
|
||||
// routing apply immediately.
|
||||
if v.manage_server != cfg.cmf.manage_server
|
||||
|| v.local_model != cfg.cmf.local_model
|
||||
|| v.threads != cfg.cmf.threads
|
||||
|| v.gpu != cfg.cmf.gpu
|
||||
|| v.servers != cfg.cmf.servers
|
||||
{
|
||||
needs_restart = true;
|
||||
}
|
||||
let old_servers = cfg.cmf.effective_servers();
|
||||
cfg.cmf = v;
|
||||
// A managed row supersedes a same-id STATIC pool entry that points at a
|
||||
// local server (the shape Import→register creates): drop the static one
|
||||
// instead of failing validation with "duplicate managed model id".
|
||||
let managed_ids: std::collections::HashSet<String> = cfg
|
||||
.cmf
|
||||
.effective_servers()
|
||||
.into_iter()
|
||||
.map(|s| s.id)
|
||||
.collect();
|
||||
cfg.models.retain(|m| {
|
||||
!(managed_ids.contains(&m.id)
|
||||
&& (m.base_url.contains("//127.0.0.1:") || m.base_url.contains("//localhost:")))
|
||||
});
|
||||
// Replacing or renaming a managed model must not brick the save: routing
|
||||
// may still reference the old id. Map a vanished id to its replacement
|
||||
// (the new server on the same port), strip ids that are truly gone, and
|
||||
// repair the default so validate() passes.
|
||||
heal_routing_after_cmf_change(&mut cfg, &old_servers);
|
||||
// Server-list changes apply LIVE after a successful reload: removed /
|
||||
// redefined servers are stopped (RAM freed), new ones are spawned.
|
||||
if cfg.cmf.effective_servers() != old_servers {
|
||||
cmf_apply = Some((old_servers, cfg.cmf.clone()));
|
||||
}
|
||||
}
|
||||
state.reload(cfg)?;
|
||||
ok(json!({ "ok": true, "needs_restart": needs_restart }))
|
||||
let applied_live = cmf_apply.is_some();
|
||||
if let Some((old, new_cfg)) = cmf_apply {
|
||||
tokio::spawn(crate::cmf_runtime::apply_server_changes(
|
||||
state.cmf.clone(),
|
||||
old,
|
||||
new_cfg,
|
||||
));
|
||||
}
|
||||
ok(json!({ "ok": true, "needs_restart": needs_restart, "applied_live": applied_live }))
|
||||
}
|
||||
|
||||
/// After the managed-servers list changed, rewrite [routing] so it only
|
||||
|
||||
@@ -173,6 +173,31 @@ impl CmfRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure a status slot exists for `server` (used when servers are added at
|
||||
/// runtime); refreshes model/port if the slot already exists.
|
||||
fn add_server_slot(&self, server: &CmfServer, host: &str) {
|
||||
let mut s = self.status.lock().unwrap();
|
||||
s.enabled = true;
|
||||
if let Some(sv) = s.servers.iter_mut().find(|x| x.id == server.id) {
|
||||
sv.model = server.model.clone();
|
||||
sv.port = server.port;
|
||||
sv.base_url = format!("http://{host}:{}/v1", server.port);
|
||||
sv.running = false;
|
||||
sv.healthy = false;
|
||||
sv.last_error = None;
|
||||
} else {
|
||||
s.servers.push(CmfServerStatus {
|
||||
id: server.id.clone(),
|
||||
model: server.model.clone(),
|
||||
port: server.port,
|
||||
base_url: format!("http://{host}:{}/v1", server.port),
|
||||
running: false,
|
||||
healthy: false,
|
||||
last_error: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop and forget a single managed server (used when a model is deleted).
|
||||
pub async fn stop_one(&self, id: &str) {
|
||||
let removed = {
|
||||
@@ -327,6 +352,62 @@ async fn wait_healthy(host: &str, port: u16, tries: u32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Apply a changed managed-servers list LIVE (no gateway restart): stop servers
|
||||
/// that were removed or redefined — freeing their memory immediately — and
|
||||
/// spawn servers that are new or redefined.
|
||||
pub async fn apply_server_changes(rt: Arc<CmfRuntime>, old: Vec<CmfServer>, cfg: CmfCfg) {
|
||||
let new = cfg.effective_servers();
|
||||
// 1. Stop what's gone or changed (kills the child → RAM released).
|
||||
for o in &old {
|
||||
let unchanged = new.iter().any(|n| n == o);
|
||||
if !unchanged {
|
||||
rt.stop_one(&o.id).await;
|
||||
rt.log(format!("stopped '{}' (removed or changed)", o.id));
|
||||
}
|
||||
}
|
||||
if new.is_empty() {
|
||||
return;
|
||||
}
|
||||
// 2. Spawn what's new or changed.
|
||||
if installed_version(&cfg.cortiq_bin).is_none() {
|
||||
rt.fail("cortiq binary not found — cannot start local model servers");
|
||||
return;
|
||||
}
|
||||
for n in &new {
|
||||
let unchanged = old.iter().any(|o| o == n);
|
||||
if unchanged {
|
||||
continue; // already running with the same definition
|
||||
}
|
||||
rt.add_server_slot(n, &cfg.local_host);
|
||||
if !std::path::Path::new(&n.model).exists() {
|
||||
rt.fail_server(&n.id, format!("model file not found: {}", n.model));
|
||||
continue;
|
||||
}
|
||||
rt.log(format!(
|
||||
"starting: cortiq serve {} --host {} --port {}",
|
||||
n.model, cfg.local_host, n.port
|
||||
));
|
||||
match spawn_serve(n, &cfg) {
|
||||
Ok(child) => rt.add_child(n.id.clone(), child),
|
||||
Err(e) => {
|
||||
rt.fail_server(&n.id, format!("failed to spawn: {e}"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (rtc, host, id, port) = (rt.clone(), cfg.local_host.clone(), n.id.clone(), n.port);
|
||||
tokio::spawn(async move {
|
||||
if wait_healthy(&host, port, 240).await {
|
||||
rtc.set_healthy(&id, true);
|
||||
rtc.log(format!(
|
||||
"local CMF server ready — '{id}' at http://{host}:{port}/v1"
|
||||
));
|
||||
} else {
|
||||
rtc.fail_server(&id, "did not become healthy in time");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Orchestrate the whole lifecycle: ensure installed → maybe update → spawn each
|
||||
/// configured local model → wait healthy. Safe to call in a background task; it
|
||||
/// never panics.
|
||||
|
||||
+11
-1
@@ -682,9 +682,19 @@ impl Config {
|
||||
self.models.iter().map(|m| m.id.as_str()).collect();
|
||||
// managed local servers are known ids too; also enforce unique ports/ids
|
||||
let mut ports = std::collections::HashSet::new();
|
||||
let mut managed_seen = std::collections::HashSet::new();
|
||||
for s in &servers {
|
||||
if !managed_seen.insert(s.id.as_str()) {
|
||||
anyhow::bail!(
|
||||
"config: two managed local models share the id '{}' — give one of the rows a different id",
|
||||
s.id
|
||||
);
|
||||
}
|
||||
if !ids.insert(s.id.as_str()) {
|
||||
anyhow::bail!("config: duplicate managed model id '{}'", s.id);
|
||||
anyhow::bail!(
|
||||
"config: managed local model '{}' has the same id as a pool model — rename the row or delete the pool entry",
|
||||
s.id
|
||||
);
|
||||
}
|
||||
if !ports.insert(s.port) {
|
||||
anyhow::bail!("config: two managed models share port {}", s.port);
|
||||
|
||||
Vendored
+1
-1
@@ -264,7 +264,7 @@ export default {
|
||||
"settings.cmf.pick": "select a model",
|
||||
"settings.cmf.models": "Local models",
|
||||
"settings.cmf.modelsHelp": "Each row is one model the gateway runs (its own cortiq serve on its own port) and registers in the pool. Add as many as you have .cmf files and free ports.",
|
||||
"settings.cmf.rowHint": "id · .cmf file · port · threads · GPU · O(1). Changes apply on restart; deleting a row stops that server.",
|
||||
"settings.cmf.rowHint": "id · .cmf file · port · threads · GPU · O(1). Applied on save: a removed row stops its server (memory freed), a new row starts one.",
|
||||
"settings.cmf.o1Auto": "O(1): auto",
|
||||
"settings.cmf.o1Help": "O(1) attention at serve time: auto — follow the hint recorded in the .cmf; off — force exact attention (fixes incoherent output after a too-aggressive --o1 conversion, no re-convert needed); all/deepN — force-enable on those layers.",
|
||||
"settings.cmf.addServer": "+ Add local model",
|
||||
|
||||
Vendored
+1
-1
@@ -263,7 +263,7 @@ export default {
|
||||
"settings.cmf.pick": "выберите модель",
|
||||
"settings.cmf.models": "Локальные модели",
|
||||
"settings.cmf.modelsHelp": "Каждая строка — отдельная модель: шлюз поднимает свой cortiq serve на своём порту и регистрирует её в пуле. Добавляйте столько, сколько есть .cmf-файлов и свободных портов.",
|
||||
"settings.cmf.rowHint": "id · файл .cmf · порт · потоки · GPU · O(1). Изменения применяются после перезапуска; удаление строки останавливает сервер.",
|
||||
"settings.cmf.rowHint": "id · файл .cmf · порт · потоки · GPU · O(1). Применяется сразу при сохранении: удалённая строка останавливает сервер (память освобождается), новая — запускает.",
|
||||
"settings.cmf.o1Auto": "O(1): авто",
|
||||
"settings.cmf.o1Help": "O(1)-внимание при запуске: авто — использовать подсказку из .cmf; off — принудительно точное внимание (лечит бессвязный вывод после слишком агрессивной --o1 конвертации, без переконвертации); all/deepN — принудительно включить на этих слоях.",
|
||||
"settings.cmf.addServer": "+ Добавить модель",
|
||||
|
||||
Reference in New Issue
Block a user