fix(model-server): deadline de warmup configurable pour le cold-start llama.cpp (#55)

Corrige la régression high « Error on loading local model » : le premier chargement
du serveur modèle local (cold-start llama.cpp) dépassait la fenêtre de readiness et
échouait. Le warmup dispose désormais d'un deadline par défaut de 600 s, surchargable
par config optionnelle `warmup_deadline_secs` (validée dans [30, 1800]).

- domain: champ `warmup_deadline_secs: Option<u64>` + validation de borne
- application: policy de readiness effective (défaut 600 s, override par config)
- app-tauri: DTO `warmupDeadlineSecs`
- infrastructure: application de la deadline effective au warmup

Verdict QA (vert) : domain 252, application 81 + 22 model_server, app-tauri
dto_model_servers 6, infrastructure model_server 2, build OK. Contrat readiness
couvert sur ports mockés.
Caveat : le cold-start end-to-end réel (llama-server) sort du sandbox de test ->
vérification manuelle utilisateur restante, non couverte par les tests unitaires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 08:41:44 +02:00
parent ce5aa2811c
commit f7cae4c0a8
7 changed files with 200 additions and 7 deletions

View File

@ -1057,6 +1057,9 @@ pub struct ModelServerConfigDto {
pub auto_start: bool,
/// Stop policy.
pub stop_policy: StopPolicyDto,
/// Optional readiness warmup deadline override in seconds.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub warmup_deadline_secs: Option<u64>,
}
impl ModelServerConfigDto {
@ -1080,6 +1083,7 @@ impl ModelServerConfigDto {
args: config.args,
auto_start: config.auto_start,
stop_policy: config.stop_policy.into(),
warmup_deadline_secs: config.warmup_deadline_secs,
}
}
@ -1122,6 +1126,7 @@ impl ModelServerConfigDto {
self.auto_start,
self.stop_policy.into(),
)
.and_then(|config| config.with_warmup_deadline_secs(self.warmup_deadline_secs))
.map_err(invalid_domain_error)
}
}

View File

@ -59,6 +59,7 @@ fn model_server_dto_serialises_flat_camelcase_wire_shape() {
assert_eq!(value["contextSize"], 4096);
assert_eq!(value["jinja"], true);
assert_eq!(value["stopPolicy"], "stopOnAppExit");
assert!(value.get("warmupDeadlineSecs").is_none());
assert!(value.get("model").is_none(), "domain model must not leak");
assert!(
value.get("endpoint").is_none(),
@ -101,6 +102,7 @@ fn model_server_dto_deserialises_flat_wire_shape_and_generates_model_id() {
assert_eq!(input.config.options.gpu_layers, Some(35));
assert_eq!(input.config.options.context_size, Some(4096));
assert_eq!(input.config.options.host, "127.0.0.1");
assert_eq!(input.config.warmup_deadline_secs, None);
assert!(!input.config.options.jinja);
assert_eq!(input.config.model.served_name, "qwen3-coder");
assert_eq!(input.config.model.label, "qwen3-coder");
@ -129,12 +131,42 @@ fn model_server_dto_preserves_existing_internal_model_id_on_upsert() {
args: Vec::new(),
auto_start: true,
stop_policy: StopPolicyDto::StopOnAppExit,
warmup_deadline_secs: Some(900),
};
let config = dto.into_domain(Some(&existing)).unwrap();
assert_eq!(config.model.id, "stable-internal-id");
assert_eq!(config.model.served_name, "qwen3-coder-updated");
assert_eq!(config.warmup_deadline_secs, Some(900));
}
#[test]
fn model_server_dto_rejects_invalid_warmup_deadline_secs() {
let raw = json!({
"id": sid(40).to_string(),
"kind": "llamaCpp",
"name": "Local Qwen",
"baseURL": "http://localhost:8085",
"port": 8085,
"modelPath": "/models/qwen.gguf",
"servedModelName": "qwen3-coder",
"host": "127.0.0.1",
"jinja": false,
"args": [],
"autoStart": true,
"stopPolicy": "stopOnAppExit",
"warmupDeadlineSecs": 29
});
let request = SaveModelServerRequestDto {
config: serde_json::from_value(raw).unwrap(),
};
assert_eq!(
save_model_server_input(request, None).unwrap_err().code,
"INVALID"
);
}
#[test]