feat(model-server): progression fine du téléchargement du modèle llamacpp (#54)

Stretch B2/F2 de #54, par-dessus le MVP déjà mergé (B1/F1).

Backend : le port de téléchargement HF publie une progression débouncée
(bytes reçus / total, pourcentage) via le stream de statut du serveur
modèle, avec gestion du total inconnu (pas de faux %), du cache hit,
de l'annulation et du timeout.

Frontend : l'overlay plein-cellule de préparation du serveur affiche la
progression réelle (barre, %, octets, source) en mappant le fil de
statut, avec la règle « pas de faux % » quand le total est inconnu.

Tests : application + infrastructure (téléchargement débouncé, cancel,
timeout, cache hit, total inconnu) et vitest (overlay + formatage pur).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:37:14 +02:00
parent bd335c3a1c
commit 2183dfd291
15 changed files with 1368 additions and 87 deletions

View File

@ -22,6 +22,7 @@
//! Each port is **fine-grained** (Interface Segregation): `FileSystem`,
//! `PtyPort`, `ProcessSpawner` are separate, never a `System` god-trait.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use async_trait::async_trait;
@ -42,7 +43,9 @@ use crate::issue::{
};
use crate::markdown::MarkdownDoc;
use crate::memory::{Memory, MemoryIndexEntry, MemoryLink, MemorySlug};
use crate::model_server::{LocalModelServerConfig, ModelServerEndpoint, ModelServerStatus};
use crate::model_server::{
HfModelRef, LocalModelServerConfig, ModelPath, ModelServerEndpoint, ModelServerStatus,
};
use crate::permission::ProjectPermissions;
use crate::profile::{AgentProfile, EmbedderProfile};
use crate::project::{Project, ProjectPath};
@ -582,6 +585,9 @@ pub enum ModelServerError {
/// Store operation failed.
#[error("store failed: {0}")]
Store(String),
/// Operation was cancelled by the caller.
#[error("cancelled")]
Cancelled,
/// Readiness timed out.
#[error("readiness timed out")]
Timeout,
@ -1013,6 +1019,66 @@ pub trait ModelServerProbe: Send + Sync {
) -> Result<ModelServerStatus, ModelServerError>;
}
/// Progress observed while resolving/downloading a model artifact.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelArtifactProgress {
/// Downloaded bytes when known.
pub downloaded_bytes: Option<u64>,
/// Total bytes when known.
pub total_bytes: Option<u64>,
/// Source being resolved, usually the Hugging Face repo reference.
pub source: Option<String>,
}
/// Resolved local model artifact.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelArtifactResolution {
/// Absolute local path to the resolved `.gguf` file.
pub path: ModelPath,
/// `true` when the artifact was already available in cache.
pub cache_hit: bool,
}
/// Cooperative cancellation token for model artifact resolution.
#[derive(Debug, Clone, Default)]
pub struct ModelArtifactCancel {
cancelled: Arc<AtomicBool>,
}
impl ModelArtifactCancel {
/// Creates a non-cancelled token.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Requests cancellation.
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
/// Returns whether cancellation was requested.
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
}
/// Resolves or downloads a model artifact before starting a model server.
#[async_trait]
pub trait ModelArtifactDownloader: Send + Sync {
/// Resolves a Hugging Face model to a local artifact path.
///
/// # Errors
/// [`ModelServerError`] when resolution/download fails or is cancelled.
async fn resolve_hf_model(
&self,
repo: &HfModelRef,
progress: Arc<dyn Fn(ModelArtifactProgress) + Send + Sync>,
cancel: ModelArtifactCancel,
) -> Result<ModelArtifactResolution, ModelServerError>;
}
/// Manages local long-lived child processes.
#[async_trait]
pub trait ManagedProcess: Send + Sync {