feat(model-server): afficher le téléchargement du modèle llamacpp au démarrage (#54)
Ajoute un handle du téléchargement des modèles lors du démarrage de llamacpp : le domaine et l'application émettent la progression de téléchargement du modèle, relayée en événement côté app-tauri, et l'UI l'affiche via un badge de lancement et un overlay de cellule pendant que le serveur de modèle démarre. Backend (B1) : progression de téléchargement dans domain/application, relais d'événement app-tauri, couverture de tests. Frontend (F1) : modelServerLaunch, badge et overlay LayoutGrid, tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -36,6 +36,18 @@ pub enum ModelServerStatusDto {
|
||||
Probing,
|
||||
/// IdeA is starting the process.
|
||||
Starting,
|
||||
/// Server is downloading/preparing a remote model.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Downloading {
|
||||
/// Downloaded bytes when known.
|
||||
downloaded_bytes: Option<u64>,
|
||||
/// Total bytes when known.
|
||||
total_bytes: Option<u64>,
|
||||
/// Completion percentage when known.
|
||||
percent: Option<f32>,
|
||||
/// Remote model source.
|
||||
source: Option<String>,
|
||||
},
|
||||
/// Server is ready.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Ready {
|
||||
@ -58,6 +70,17 @@ impl From<&ModelServerLifecycleStatus> for ModelServerStatusDto {
|
||||
ModelServerLifecycleStatus::NotConfigured => Self::NotConfigured,
|
||||
ModelServerLifecycleStatus::Probing => Self::Probing,
|
||||
ModelServerLifecycleStatus::Starting => Self::Starting,
|
||||
ModelServerLifecycleStatus::Downloading {
|
||||
downloaded_bytes,
|
||||
total_bytes,
|
||||
percent,
|
||||
source,
|
||||
} => Self::Downloading {
|
||||
downloaded_bytes: *downloaded_bytes,
|
||||
total_bytes: *total_bytes,
|
||||
percent: *percent,
|
||||
source: source.clone(),
|
||||
},
|
||||
ModelServerLifecycleStatus::Ready { reused } => Self::Ready { reused: *reused },
|
||||
ModelServerLifecycleStatus::Failed { code, message } => Self::Failed {
|
||||
code: code.clone(),
|
||||
@ -1168,6 +1191,31 @@ mod tests {
|
||||
assert_eq!(json["status"]["reused"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_server_status_changed_relays_downloading_to_dto_and_wire() {
|
||||
let dto = DomainEventDto::from(&DomainEvent::ModelServerStatusChanged {
|
||||
server_id: server(37),
|
||||
status: ModelServerLifecycleStatus::Downloading {
|
||||
downloaded_bytes: None,
|
||||
total_bytes: None,
|
||||
percent: None,
|
||||
source: Some("Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF".to_owned()),
|
||||
},
|
||||
});
|
||||
|
||||
let json = serde_json::to_value(&dto).expect("serialisable");
|
||||
assert_eq!(json["type"], "modelServerStatusChanged");
|
||||
assert_eq!(json["serverId"], server(37).to_string());
|
||||
assert_eq!(json["status"]["state"], "downloading");
|
||||
assert_eq!(json["status"]["downloadedBytes"], serde_json::Value::Null);
|
||||
assert_eq!(json["status"]["totalBytes"], serde_json::Value::Null);
|
||||
assert_eq!(json["status"]["percent"], serde_json::Value::Null);
|
||||
assert_eq!(
|
||||
json["status"]["source"],
|
||||
"Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_launch_failed_relays_model_server_cause() {
|
||||
let dto = DomainEventDto::from(&DomainEvent::AgentLaunchFailed {
|
||||
|
||||
@ -15,6 +15,7 @@ use domain::ports::{
|
||||
};
|
||||
use domain::{LocalModelServerId, StopPolicy};
|
||||
use tokio::sync::{Mutex as AsyncMutex, Notify};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
@ -161,6 +162,13 @@ impl Default for ReadinessPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Default upper bound for Hugging Face model download/preparation.
|
||||
///
|
||||
/// llama.cpp may download multi-GB models before binding the OpenAI-compatible
|
||||
/// endpoint. The short readiness window remains for local files; HF sources use
|
||||
/// this separate deadline while the process is still alive.
|
||||
pub const DEFAULT_HF_DOWNLOAD_DEADLINE: Duration = Duration::from_secs(30 * 60);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ActiveServer {
|
||||
handle: ManagedProcessHandle,
|
||||
@ -208,6 +216,7 @@ pub struct EnsureLocalModelServer {
|
||||
active: Mutex<HashMap<LocalModelServerId, ActiveServer>>,
|
||||
inflight: AsyncMutex<HashMap<LocalModelServerId, Arc<InflightEnsure>>>,
|
||||
readiness: ReadinessPolicy,
|
||||
hf_download_deadline: Duration,
|
||||
}
|
||||
|
||||
impl EnsureLocalModelServer {
|
||||
@ -232,6 +241,7 @@ impl EnsureLocalModelServer {
|
||||
active: Mutex::new(HashMap::new()),
|
||||
inflight: AsyncMutex::new(HashMap::new()),
|
||||
readiness: ReadinessPolicy::default(),
|
||||
hf_download_deadline: DEFAULT_HF_DOWNLOAD_DEADLINE,
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,6 +252,13 @@ impl EnsureLocalModelServer {
|
||||
self
|
||||
}
|
||||
|
||||
/// Overrides the long Hugging Face download/preparation deadline.
|
||||
#[must_use]
|
||||
pub fn with_hf_download_deadline(mut self, deadline: Duration) -> Self {
|
||||
self.hf_download_deadline = deadline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ensures the server is reachable.
|
||||
///
|
||||
/// # Errors
|
||||
@ -333,10 +350,21 @@ impl EnsureLocalModelServer {
|
||||
},
|
||||
);
|
||||
|
||||
let hf_source = hf_source(&config);
|
||||
self.wait_for_started_server(&config, &handle, hf_source)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn wait_for_started_server(
|
||||
&self,
|
||||
config: &LocalModelServerConfig,
|
||||
handle: &ManagedProcessHandle,
|
||||
hf_source: Option<String>,
|
||||
) -> Result<EnsureLocalModelServerOutput, AppError> {
|
||||
for attempt in 0..self.readiness.attempts {
|
||||
match self.probe.probe(&config.endpoint).await {
|
||||
Err(err) => {
|
||||
self.stop_started_server(config.id, &handle).await;
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
}
|
||||
Ok(ModelServerStatus::ReadyReused | ModelServerStatus::ReadyStarted) => {
|
||||
@ -345,7 +373,7 @@ impl EnsureLocalModelServer {
|
||||
ModelServerLifecycleStatus::Ready { reused: false },
|
||||
);
|
||||
return Ok(EnsureLocalModelServerOutput {
|
||||
ready: ready(&config, ModelServerStatus::ReadyStarted),
|
||||
ready: ready(config, ModelServerStatus::ReadyStarted),
|
||||
});
|
||||
}
|
||||
Ok(ModelServerStatus::Unreachable) => {
|
||||
@ -356,9 +384,83 @@ impl EnsureLocalModelServer {
|
||||
}
|
||||
}
|
||||
|
||||
let err = ModelServerError::Timeout;
|
||||
self.stop_started_server(config.id, &handle).await;
|
||||
self.fail(config.id, err)
|
||||
let Some(source) = hf_source else {
|
||||
let err = ModelServerError::Timeout;
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
};
|
||||
|
||||
match self.process.status(handle).await {
|
||||
Ok(ProcessStatus::Running) => {
|
||||
self.publish(
|
||||
config.id,
|
||||
ModelServerLifecycleStatus::Downloading {
|
||||
downloaded_bytes: None,
|
||||
total_bytes: None,
|
||||
percent: None,
|
||||
source: Some(source.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(ProcessStatus::Exited { code }) => {
|
||||
self.active.lock().unwrap().remove(&config.id);
|
||||
return self.fail(config.id, premature_exit_error(code));
|
||||
}
|
||||
Ok(ProcessStatus::Unknown) => {
|
||||
self.active.lock().unwrap().remove(&config.id);
|
||||
return self.fail(
|
||||
config.id,
|
||||
ModelServerError::Process("process status unknown".to_owned()),
|
||||
);
|
||||
}
|
||||
Err(err) => return self.fail(config.id, err),
|
||||
}
|
||||
|
||||
let deadline = Instant::now() + self.hf_download_deadline;
|
||||
loop {
|
||||
if Instant::now() >= deadline {
|
||||
let err = ModelServerError::Timeout;
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
}
|
||||
match self.probe.probe(&config.endpoint).await {
|
||||
Err(err) => {
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
}
|
||||
Ok(ModelServerStatus::ReadyReused | ModelServerStatus::ReadyStarted) => {
|
||||
self.publish(
|
||||
config.id,
|
||||
ModelServerLifecycleStatus::Ready { reused: false },
|
||||
);
|
||||
return Ok(EnsureLocalModelServerOutput {
|
||||
ready: ready(config, ModelServerStatus::ReadyStarted),
|
||||
});
|
||||
}
|
||||
Ok(ModelServerStatus::Unreachable) => {}
|
||||
}
|
||||
match self.process.status(handle).await {
|
||||
Ok(ProcessStatus::Running) => {
|
||||
if !self.readiness.backoff.is_zero() {
|
||||
tokio::time::sleep(self.readiness.backoff).await;
|
||||
} else {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
Ok(ProcessStatus::Exited { code }) => {
|
||||
self.active.lock().unwrap().remove(&config.id);
|
||||
return self.fail(config.id, premature_exit_error(code));
|
||||
}
|
||||
Ok(ProcessStatus::Unknown) => {
|
||||
self.active.lock().unwrap().remove(&config.id);
|
||||
return self.fail(
|
||||
config.id,
|
||||
ModelServerError::Process("process status unknown".to_owned()),
|
||||
);
|
||||
}
|
||||
Err(err) => return self.fail(config.id, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops active servers whose policy is [`StopPolicy::StopOnAppExit`].
|
||||
@ -497,6 +599,20 @@ fn ready(config: &LocalModelServerConfig, status: ModelServerStatus) -> ModelSer
|
||||
}
|
||||
}
|
||||
|
||||
fn hf_source(config: &LocalModelServerConfig) -> Option<String> {
|
||||
match config.model.source.as_ref()? {
|
||||
ModelSource::HuggingFace { repo } => Some(repo.as_str().to_owned()),
|
||||
ModelSource::LocalPath { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn premature_exit_error(code: Option<i32>) -> ModelServerError {
|
||||
ModelServerError::Process(match code {
|
||||
Some(code) => format!("model server exited before readiness with code {code}"),
|
||||
None => "model server exited before readiness".to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Stable model-server error code for event/DTO mapping.
|
||||
#[must_use]
|
||||
pub fn model_server_error_code(err: &ModelServerError) -> &'static str {
|
||||
|
||||
@ -12,8 +12,9 @@ use application::{
|
||||
};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::model_server::{
|
||||
ExecutablePath, LlamaCppOptions, LocalModelRef, LocalModelServerConfig, LocalModelServerKind,
|
||||
ModelPath, ModelServerEndpoint, ModelServerStatus, ModelSource, StopPolicy,
|
||||
ExecutablePath, HfModelRef, LlamaCppOptions, LocalModelRef, LocalModelServerConfig,
|
||||
LocalModelServerKind, ModelPath, ModelServerEndpoint, ModelServerLifecycleStatus,
|
||||
ModelServerStatus, ModelSource, StopPolicy,
|
||||
};
|
||||
use domain::ports::{
|
||||
DirEntry, EventBus, EventStream, FileSystem, FsError, ManagedProcess, ManagedProcessHandle,
|
||||
@ -56,6 +57,30 @@ fn config(
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn hf_config(id: LocalModelServerId, port: u16, repo: &str) -> LocalModelServerConfig {
|
||||
LocalModelServerConfig::new(
|
||||
id,
|
||||
LocalModelServerKind::LlamaCpp,
|
||||
"llama.cpp",
|
||||
ModelServerEndpoint::new(format!("http://localhost:{port}"), port).unwrap(),
|
||||
LocalModelRef::new(
|
||||
"qwen",
|
||||
"Qwen",
|
||||
Some(ModelSource::HuggingFace {
|
||||
repo: HfModelRef::new(repo).unwrap(),
|
||||
}),
|
||||
"qwen3-coder-30b",
|
||||
)
|
||||
.unwrap(),
|
||||
Some(ExecutablePath::new("llama-server").unwrap()),
|
||||
LlamaCppOptions::default(),
|
||||
Vec::new(),
|
||||
true,
|
||||
StopPolicy::StopOnAppExit,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeRegistry(Mutex<HashMap<LocalModelServerId, LocalModelServerConfig>>);
|
||||
|
||||
@ -158,6 +183,7 @@ struct FakeProcess {
|
||||
spawns: Mutex<Vec<SpawnSpec>>,
|
||||
kills: Mutex<Vec<String>>,
|
||||
statuses: Mutex<HashMap<String, ProcessStatus>>,
|
||||
status_sequence: Mutex<VecDeque<ProcessStatus>>,
|
||||
spawn_delay: Duration,
|
||||
}
|
||||
|
||||
@ -187,6 +213,9 @@ impl ManagedProcess for FakeProcess {
|
||||
&self,
|
||||
handle: &ManagedProcessHandle,
|
||||
) -> Result<ProcessStatus, ModelServerError> {
|
||||
if let Some(status) = self.status_sequence.lock().unwrap().pop_front() {
|
||||
return Ok(status);
|
||||
}
|
||||
Ok(*self
|
||||
.statuses
|
||||
.lock()
|
||||
@ -203,25 +232,35 @@ impl ModelServerRuntime for FakeRuntime {
|
||||
&self,
|
||||
config: &LocalModelServerConfig,
|
||||
) -> Result<ModelServerArgv, ModelServerError> {
|
||||
let Some(ModelSource::LocalPath { path }) = config.model.source.as_ref() else {
|
||||
return Err(ModelServerError::PathNotAccessible(
|
||||
"model.source missing".to_owned(),
|
||||
));
|
||||
};
|
||||
let mut args = Vec::new();
|
||||
match config
|
||||
.model
|
||||
.source
|
||||
.as_ref()
|
||||
.ok_or_else(|| ModelServerError::PathNotAccessible("model.source missing".to_owned()))?
|
||||
{
|
||||
ModelSource::LocalPath { path } => {
|
||||
args.push("--model".to_owned());
|
||||
args.push(path.as_str().to_owned());
|
||||
}
|
||||
ModelSource::HuggingFace { repo } => {
|
||||
args.push("-hf".to_owned());
|
||||
args.push(repo.as_str().to_owned());
|
||||
}
|
||||
}
|
||||
args.extend([
|
||||
"--port".to_owned(),
|
||||
config.endpoint.port.to_string(),
|
||||
"--host".to_owned(),
|
||||
config.options.host.clone(),
|
||||
]);
|
||||
Ok(ModelServerArgv {
|
||||
command: config
|
||||
.binary
|
||||
.as_ref()
|
||||
.map(|binary| binary.as_str().to_owned())
|
||||
.unwrap_or_else(|| "llama-server".to_owned()),
|
||||
args: vec![
|
||||
"--model".to_owned(),
|
||||
path.as_str().to_owned(),
|
||||
"--port".to_owned(),
|
||||
config.endpoint.port.to_string(),
|
||||
"--host".to_owned(),
|
||||
config.options.host.clone(),
|
||||
],
|
||||
args,
|
||||
})
|
||||
}
|
||||
|
||||
@ -303,6 +342,7 @@ fn ensure(
|
||||
attempts: 2,
|
||||
backoff: Duration::ZERO,
|
||||
})
|
||||
.with_hf_download_deadline(Duration::from_secs(5))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -546,6 +586,158 @@ async fn readiness_timeout_kills_started_process() {
|
||||
assert_eq!(process.kills.lock().unwrap().as_slice(), ["h1"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hf_unreachable_alive_publishes_downloading_without_short_timeout_kill() {
|
||||
let registry = Arc::new(FakeRegistry::default());
|
||||
registry
|
||||
.save(hf_config(
|
||||
sid(11),
|
||||
8091,
|
||||
"Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let process = Arc::new(FakeProcess::default());
|
||||
let events = Arc::new(FakeEvents::default());
|
||||
let usecase = ensure(
|
||||
Arc::clone(®istry),
|
||||
Arc::new(FakeProbe::new(vec![
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::ReadyReused,
|
||||
])),
|
||||
Arc::clone(&process),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::clone(&events),
|
||||
);
|
||||
|
||||
let out = usecase
|
||||
.execute(EnsureLocalModelServerInput { server_id: sid(11) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.ready.status, ModelServerStatus::ReadyStarted);
|
||||
assert!(process.kills.lock().unwrap().is_empty());
|
||||
assert!(events.0.lock().unwrap().iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
DomainEvent::ModelServerStatusChanged {
|
||||
server_id,
|
||||
status: ModelServerLifecycleStatus::Downloading {
|
||||
downloaded_bytes: None,
|
||||
total_bytes: None,
|
||||
percent: None,
|
||||
source: Some(source),
|
||||
},
|
||||
} if *server_id == sid(11)
|
||||
&& source == "Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF"
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hf_download_phase_reports_ready_when_probe_becomes_ok() {
|
||||
let registry = Arc::new(FakeRegistry::default());
|
||||
registry
|
||||
.save(hf_config(
|
||||
sid(12),
|
||||
8092,
|
||||
"Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let process = Arc::new(FakeProcess::default());
|
||||
let events = Arc::new(FakeEvents::default());
|
||||
let usecase = ensure(
|
||||
Arc::clone(®istry),
|
||||
Arc::new(FakeProbe::new(vec![
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::ReadyStarted,
|
||||
])),
|
||||
Arc::clone(&process),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::clone(&events),
|
||||
);
|
||||
|
||||
let out = usecase
|
||||
.execute(EnsureLocalModelServerInput { server_id: sid(12) })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.ready.status, ModelServerStatus::ReadyStarted);
|
||||
let statuses: Vec<ModelServerLifecycleStatus> = events
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|event| match event {
|
||||
DomainEvent::ModelServerStatusChanged { status, .. } => Some(status.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(matches!(
|
||||
statuses.as_slice(),
|
||||
[
|
||||
ModelServerLifecycleStatus::Probing,
|
||||
ModelServerLifecycleStatus::Starting,
|
||||
ModelServerLifecycleStatus::Downloading { .. },
|
||||
ModelServerLifecycleStatus::Ready { reused: false },
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hf_download_phase_fails_when_process_exits() {
|
||||
let registry = Arc::new(FakeRegistry::default());
|
||||
registry
|
||||
.save(hf_config(
|
||||
sid(13),
|
||||
8093,
|
||||
"Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF",
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let process = Arc::new(FakeProcess::default());
|
||||
process
|
||||
.status_sequence
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_back(ProcessStatus::Exited { code: Some(42) });
|
||||
let events = Arc::new(FakeEvents::default());
|
||||
let usecase = ensure(
|
||||
Arc::clone(®istry),
|
||||
Arc::new(FakeProbe::new(vec![
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::Unreachable,
|
||||
ModelServerStatus::Unreachable,
|
||||
])),
|
||||
Arc::clone(&process),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::clone(&events),
|
||||
);
|
||||
|
||||
let err = usecase
|
||||
.execute(EnsureLocalModelServerInput { server_id: sid(13) })
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("process"));
|
||||
assert!(err.to_string().contains("42"));
|
||||
assert!(process.kills.lock().unwrap().is_empty());
|
||||
assert!(events.0.lock().unwrap().iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
DomainEvent::ModelServerStatusChanged {
|
||||
status: ModelServerLifecycleStatus::Failed { code, .. },
|
||||
..
|
||||
} if code == "process"
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_registry_entry_is_model_server_not_configured() {
|
||||
let usecase = ensure(
|
||||
|
||||
@ -33,7 +33,7 @@ pub enum OrchestrationSource {
|
||||
/// concern relayed to IPC by an infrastructure adapter, which owns the wire
|
||||
/// format. `PtyOutput` in particular is usually short-circuited to a Tauri
|
||||
/// channel rather than serialised here.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DomainEvent {
|
||||
/// A project was created.
|
||||
ProjectCreated {
|
||||
|
||||
@ -494,7 +494,7 @@ pub struct ModelServerReady {
|
||||
}
|
||||
|
||||
/// Lifecycle status emitted for presentation.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ModelServerLifecycleStatus {
|
||||
/// No local model server is configured for the profile.
|
||||
NotConfigured,
|
||||
@ -502,6 +502,17 @@ pub enum ModelServerLifecycleStatus {
|
||||
Probing,
|
||||
/// IdeA is starting the server.
|
||||
Starting,
|
||||
/// The server process is alive but still downloading/preparing a remote model.
|
||||
Downloading {
|
||||
/// Downloaded bytes when known.
|
||||
downloaded_bytes: Option<u64>,
|
||||
/// Total bytes when known.
|
||||
total_bytes: Option<u64>,
|
||||
/// Completion percentage when known.
|
||||
percent: Option<f32>,
|
||||
/// Remote model source, for example a Hugging Face repository id.
|
||||
source: Option<String>,
|
||||
},
|
||||
/// The server is ready.
|
||||
Ready {
|
||||
/// `true` when an already-running server was reused.
|
||||
|
||||
Reference in New Issue
Block a user