feat(model-server): backend modèles locaux — serveur llama.cpp intégré (#35) et profils OpenCode locaux multiples (#36)
Sprint « Modeles locaux », couche backend (domain/application/infra/app-tauri). #35 — Serveur de modèle local intégré (llama.cpp) : - domain: model_server.rs (agrégat + statut), ports ModelServerProbe / ManagedProcess / ModelServerRuntime / ModelServerRegistry, événements model_server_status_changed et agent_launch_failed. - application: use case EnsureLocalModelServer branché sur LaunchAgent. - infrastructure: adapters HttpOpenAiCompatibleProbe, LlamaCppRuntime, LocalManagedProcess, FsModelServerRegistry. - app-tauri: DTO plat LocalModelServerConfigDto, commandes list/save/delete_model_server avec garde model_server_in_use, wiring. #36 — Profils OpenCode locaux multiples : - domain: VO LocalModelServerId, OpenCodeConfig.local_model_server_id. - application: use case CloneOpenCodeProfileFromSeed. - app-tauri: commande clone_opencode_profile_from_seed, DTO/wiring. Tests verts (exécution réelle) : domain+application OK, app-tauri dto_model_servers 3/3 et dto_profiles 10/10, infra model_server 2/2, application model_server+profile_usecases 19/19, cargo build workspace Finished. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -13,15 +13,17 @@ use async_trait::async_trait;
|
||||
|
||||
use domain::ids::ProfileId;
|
||||
use domain::ports::{
|
||||
AgentRuntime, PreparedContext, ProfileStore, RuntimeError, SessionPlan, SpawnSpec, StoreError,
|
||||
AgentRuntime, IdGenerator, PreparedContext, ProfileStore, RuntimeError, SessionPlan, SpawnSpec,
|
||||
StoreError,
|
||||
};
|
||||
use domain::profile::{AgentProfile, ContextInjection};
|
||||
use domain::profile::{AgentProfile, ContextInjection, OpenCodeConfig, StructuredAdapter};
|
||||
use domain::project::ProjectPath;
|
||||
|
||||
use application::{
|
||||
reference_profile_id, reference_profiles, ConfigureProfiles, ConfigureProfilesInput,
|
||||
DeleteProfile, DeleteProfileInput, DetectProfiles, DetectProfilesInput, FirstRunState,
|
||||
ListProfiles, ReferenceProfiles, SaveProfile, SaveProfileInput, CODEX_SUBMIT_DELAY_MS,
|
||||
reference_profile_id, reference_profiles, CloneOpenCodeProfileFromSeed,
|
||||
CloneOpenCodeProfileFromSeedInput, ConfigureProfiles, ConfigureProfilesInput, DeleteProfile,
|
||||
DeleteProfileInput, DetectProfiles, DetectProfilesInput, FirstRunState, ListProfiles,
|
||||
ReferenceProfiles, SaveProfile, SaveProfileInput, CODEX_SUBMIT_DELAY_MS,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -127,6 +129,20 @@ impl AgentRuntime for YieldingRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
struct SeqIds(Mutex<Vec<uuid::Uuid>>);
|
||||
|
||||
impl SeqIds {
|
||||
fn new(ids: Vec<uuid::Uuid>) -> Self {
|
||||
Self(Mutex::new(ids))
|
||||
}
|
||||
}
|
||||
|
||||
impl IdGenerator for SeqIds {
|
||||
fn new_uuid(&self) -> uuid::Uuid {
|
||||
self.0.lock().unwrap().remove(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn profile(id: u128, name: &str, command: &str) -> AgentProfile {
|
||||
AgentProfile::new(
|
||||
ProfileId::from_uuid(uuid::Uuid::from_u128(id)),
|
||||
@ -327,6 +343,103 @@ async fn delete_unknown_is_not_found_error() {
|
||||
assert_eq!(err.code(), "NOT_FOUND", "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clone_opencode_profile_from_seed_creates_distinct_open_code_instance() {
|
||||
let store = FakeProfileStore::default();
|
||||
let clone = CloneOpenCodeProfileFromSeed::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(3601)])),
|
||||
);
|
||||
|
||||
let out = clone
|
||||
.execute(CloneOpenCodeProfileFromSeedInput {
|
||||
name: Some("OpenCode llama.cpp 2".to_owned()),
|
||||
opencode: Some(
|
||||
OpenCodeConfig::new(
|
||||
"http://localhost:8081/v1",
|
||||
None,
|
||||
"qwen3-coder-70b",
|
||||
Some(false),
|
||||
Some(true),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let seed_id = reference_profile_id("opencode-llamacpp");
|
||||
assert_ne!(out.profile.id, seed_id);
|
||||
assert_eq!(
|
||||
out.profile.id,
|
||||
ProfileId::from_uuid(uuid::Uuid::from_u128(3601))
|
||||
);
|
||||
assert_eq!(out.profile.name, "OpenCode llama.cpp 2");
|
||||
assert_eq!(
|
||||
out.profile.structured_adapter,
|
||||
Some(StructuredAdapter::OpenCode)
|
||||
);
|
||||
assert_eq!(
|
||||
out.profile
|
||||
.opencode
|
||||
.as_ref()
|
||||
.map(|config| config.base_url.as_str()),
|
||||
Some("http://localhost:8081/v1")
|
||||
);
|
||||
|
||||
let profiles = store.0.lock().unwrap().profiles.clone();
|
||||
assert_eq!(profiles.len(), 1);
|
||||
assert_eq!(profiles[0].id, out.profile.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clone_opencode_profile_prefers_persisted_seed_without_recreating_it() {
|
||||
let store = FakeProfileStore::default();
|
||||
let save = SaveProfile::new(Arc::new(store.clone()));
|
||||
let mut seed = reference_profiles()
|
||||
.into_iter()
|
||||
.find(|profile| profile.id == reference_profile_id("opencode-llamacpp"))
|
||||
.expect("seed exists");
|
||||
seed.name = "Edited local OpenCode seed".to_owned();
|
||||
save.execute(SaveProfileInput {
|
||||
profile: seed.clone(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let clone = CloneOpenCodeProfileFromSeed::new(
|
||||
Arc::new(store.clone()),
|
||||
Arc::new(SeqIds::new(vec![uuid::Uuid::from_u128(3602)])),
|
||||
);
|
||||
let out = clone
|
||||
.execute(CloneOpenCodeProfileFromSeedInput {
|
||||
name: None,
|
||||
opencode: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(out.profile.name, "Edited local OpenCode seed copy");
|
||||
assert_eq!(out.profile.opencode, seed.opencode);
|
||||
let profiles = store.0.lock().unwrap().profiles.clone();
|
||||
assert_eq!(
|
||||
profiles
|
||||
.iter()
|
||||
.filter(|profile| profile.id == reference_profile_id("opencode-llamacpp"))
|
||||
.count(),
|
||||
1,
|
||||
"the canonical seed must be preserved, not recreated as a duplicate"
|
||||
);
|
||||
assert_eq!(
|
||||
profiles
|
||||
.iter()
|
||||
.filter(|profile| profile.structured_adapter == Some(StructuredAdapter::OpenCode))
|
||||
.count(),
|
||||
2,
|
||||
"ProfileId is the identity: multiple OpenCode profiles can coexist"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReferenceProfiles / catalogue
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user