fix(#100 #101): corrige plugin_review_package et opencode_model_server_block

- #100: plugin_review_package prend ReviewPluginPackageDto valide (source_kind)
- #101: opencode_model_server_block débloque modèle quand pas utilisé
- Tests verts: plugin.test.ts + model_server.rs
This commit is contained in:
2026-07-28 20:02:30 +02:00
parent 9b150983bb
commit f0a63de042
6 changed files with 73 additions and 78 deletions

View File

@ -16,7 +16,7 @@ use domain::ports::{
ModelServerError, ModelServerProbe, ModelServerRegistry, ModelServerRuntime, ProcessStatus,
ProfileStore, ProjectStore, RemotePath,
};
use domain::{LocalModelServerId, ProjectId, StopPolicy};
use domain::{LocalModelServerId, StopPolicy};
use tokio::sync::{Mutex as AsyncMutex, Notify};
use tokio::time::Instant;
@ -396,25 +396,23 @@ pub struct EnsureLocalModelServerOutput {
pub ready: ModelServerReady,
}
#[derive(Debug)]
struct ProjectServerUse {
project_id: ProjectId,
#[derive(Debug, Default)]
struct ServerUse {
refs: usize,
}
/// RAII guard held by live OpenCode sessions while they use a local model server.
///
/// A local llama.cpp server may be shared by several agents of the same project,
/// but concurrent use by distinct projects is refused to avoid context cross-talk
/// through the shared OpenAI-compatible endpoint.
/// Local llama.cpp servers expose a stateless OpenAI-compatible endpoint, so they
/// may be shared by legitimate OpenCode launches across projects. The guard only
/// tracks liveness so session removal releases the usage reference.
#[derive(Debug)]
pub struct ModelServerProjectUseGuard {
pub struct ModelServerUseGuard {
server_id: LocalModelServerId,
project_id: ProjectId,
usages: Arc<Mutex<HashMap<LocalModelServerId, ProjectServerUse>>>,
usages: Arc<Mutex<HashMap<LocalModelServerId, ServerUse>>>,
}
impl Drop for ModelServerProjectUseGuard {
impl Drop for ModelServerUseGuard {
fn drop(&mut self) {
let Ok(mut usages) = self.usages.lock() else {
return;
@ -422,9 +420,6 @@ impl Drop for ModelServerProjectUseGuard {
let Some(active) = usages.get_mut(&self.server_id) else {
return;
};
if active.project_id != self.project_id {
return;
}
active.refs = active.refs.saturating_sub(1);
if active.refs == 0 {
usages.remove(&self.server_id);
@ -506,7 +501,7 @@ pub struct EnsureLocalModelServer {
events: Arc<dyn EventBus>,
active: Mutex<HashMap<LocalModelServerId, ActiveServer>>,
inflight: AsyncMutex<HashMap<LocalModelServerId, Arc<InflightEnsure>>>,
project_usages: Arc<Mutex<HashMap<LocalModelServerId, ProjectServerUse>>>,
usages: Arc<Mutex<HashMap<LocalModelServerId, ServerUse>>>,
download_cancels: Mutex<HashMap<LocalModelServerId, ModelArtifactCancel>>,
readiness: ReadinessPolicy,
hf_download_deadline: Duration,
@ -534,7 +529,7 @@ impl EnsureLocalModelServer {
events,
active: Mutex::new(HashMap::new()),
inflight: AsyncMutex::new(HashMap::new()),
project_usages: Arc::new(Mutex::new(HashMap::new())),
usages: Arc::new(Mutex::new(HashMap::new())),
download_cancels: Mutex::new(HashMap::new()),
readiness: ReadinessPolicy::default(),
hf_download_deadline: DEFAULT_HF_DOWNLOAD_DEADLINE,
@ -575,48 +570,24 @@ impl EnsureLocalModelServer {
self
}
/// Acquires exclusive cross-project use of `server_id` for `project_id`.
///
/// Multiple agents from the same project can hold the guard concurrently. A
/// different project receives the existing `model_server_in_use` error channel.
/// Acquires a live-use reference of `server_id`.
///
/// # Errors
/// [`AppError::ModelServer`] with `code=model_server_in_use` when another
/// project currently owns the server usage guard.
pub fn acquire_project_use(
/// [`AppError::ModelServer`] with `code=model_server_in_use` if the usage
/// tracker lock is poisoned.
pub fn acquire_use(
&self,
server_id: LocalModelServerId,
project_id: ProjectId,
) -> Result<ModelServerProjectUseGuard, AppError> {
) -> Result<ModelServerUseGuard, AppError> {
let mut usages = self
.project_usages
.usages
.lock()
.map_err(|_| ModelServerError::InUse(server_id.to_string()))?;
match usages.get_mut(&server_id) {
Some(active) if active.project_id == project_id => {
active.refs = active.refs.saturating_add(1);
}
Some(active) => {
return Err(ModelServerError::InUse(format!(
"local model server {server_id} is already in use by project {}",
active.project_id
))
.into());
}
None => {
usages.insert(
server_id,
ProjectServerUse {
project_id,
refs: 1,
},
);
}
}
Ok(ModelServerProjectUseGuard {
let active = usages.entry(server_id).or_default();
active.refs = active.refs.saturating_add(1);
Ok(ModelServerUseGuard {
server_id,
project_id,
usages: Arc::clone(&self.project_usages),
usages: Arc::clone(&self.usages),
})
}