feat(opencode): remplace le profil Ollama HTTP par OpenCode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -246,6 +246,8 @@ pub enum StructuredAdapter {
|
||||
Claude,
|
||||
/// Piloté par `CodexExecSession` (`codex exec` structuré).
|
||||
Codex,
|
||||
/// Piloté par OpenCode en mode process-backed (`opencode run --format json`).
|
||||
OpenCode,
|
||||
/// Piloté par l'adapter HTTP OpenAI-compatible (Ollama, llama.cpp, runtime LAN).
|
||||
OpenAiCompatible,
|
||||
}
|
||||
@ -264,11 +266,40 @@ impl StructuredAdapter {
|
||||
match self {
|
||||
Self::Claude => "claude",
|
||||
Self::Codex => "codex",
|
||||
Self::OpenCode => "opencode",
|
||||
Self::OpenAiCompatible => "openai-compatible",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration déclarative d'un profil OpenCode process-backed.
|
||||
///
|
||||
/// OpenCode reste une CLI locale pilotée par process, distincte de
|
||||
/// [`StructuredAdapter::OpenAiCompatible`] qui est un client HTTP in-process.
|
||||
/// Le modèle est optionnel : quand il est présent, IdeA l'écrit dans
|
||||
/// `opencode.json`; quand il est absent, OpenCode applique son propre défaut ou
|
||||
/// un override explicite porté par la commande utilisateur.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenCodeConfig {
|
||||
/// Modèle OpenCode complet, par exemple `ollama/qwen3-coder:30b`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
impl OpenCodeConfig {
|
||||
/// Construit une configuration OpenCode validée.
|
||||
///
|
||||
/// # Errors
|
||||
/// Renvoie [`DomainError::EmptyField`] si `model` est présent mais vide.
|
||||
pub fn new(model: Option<String>) -> Result<Self, DomainError> {
|
||||
if let Some(model) = &model {
|
||||
crate::validation::non_empty(model, "opencode.model")?;
|
||||
}
|
||||
Ok(Self { model })
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration HTTP d'un serveur de chat OpenAI-compatible.
|
||||
///
|
||||
/// Pure donnée domaine : l'endpoint est validé syntaxiquement mais jamais contacté,
|
||||
@ -417,6 +448,12 @@ pub enum McpConfigStrategy {
|
||||
/// `target` (ex. `"CODEX_HOME"`).
|
||||
home_env: String,
|
||||
},
|
||||
/// Écrire une configuration OpenCode complète (`opencode.json`) dans le run dir.
|
||||
/// Ce format porte à la fois le provider Ollama et le serveur MCP local `idea`.
|
||||
OpenCodeConfig {
|
||||
/// Chemin relatif sûr du fichier de config OpenCode.
|
||||
target: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl McpConfigStrategy {
|
||||
@ -469,6 +506,17 @@ impl McpConfigStrategy {
|
||||
crate::validation::valid_env_var(&home_env)?;
|
||||
Ok(Self::TomlConfigHome { target, home_env })
|
||||
}
|
||||
|
||||
/// Constructeur validé `OpenCodeConfig`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Renvoie [`DomainError::PathNotRelativeSafe`] si `target` est absolu ou
|
||||
/// contient `..`.
|
||||
pub fn open_code_config(target: impl Into<String>) -> Result<Self, DomainError> {
|
||||
let target = target.into();
|
||||
crate::validation::relative_safe(&target)?;
|
||||
Ok(Self::OpenCodeConfig { target })
|
||||
}
|
||||
}
|
||||
|
||||
/// Capacité MCP d'un profil : COMMENT déclarer le serveur MCP IdeA à cette CLI,
|
||||
@ -667,6 +715,11 @@ pub struct AgentProfile {
|
||||
/// tous les profils historiques et tous les adapters non HTTP.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub chat_http: Option<HttpChatConfig>,
|
||||
/// Configuration OpenCode pour [`StructuredAdapter::OpenCode`]. Distincte de
|
||||
/// [`Self::chat_http`] : OpenCode est un host process MCP natif, pas l'adapter
|
||||
/// HTTP OpenAI-compatible in-process.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opencode: Option<OpenCodeConfig>,
|
||||
/// Capacité **MCP** (ARCHITECTURE §14.3, orchestration v3, Décision 1).
|
||||
/// `None` ⇒ repli fichier `.ideai/requests` + prose (comportement actuel).
|
||||
/// `Some(_)` ⇒ IdeA matérialise la config MCP de cette CLI au lancement et
|
||||
@ -871,6 +924,7 @@ impl AgentProfile {
|
||||
session,
|
||||
structured_adapter: None,
|
||||
chat_http: None,
|
||||
opencode: None,
|
||||
mcp: None,
|
||||
liveness: None,
|
||||
rate_limit_pattern: None,
|
||||
@ -896,6 +950,13 @@ impl AgentProfile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe la configuration OpenCode process-backed.
|
||||
#[must_use]
|
||||
pub fn with_opencode(mut self, config: OpenCodeConfig) -> Self {
|
||||
self.opencode = Some(config);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe la [`McpCapability`] (§14.3, orchestration v3) et renvoie le
|
||||
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
|
||||
/// profils sans MCP ne l'appellent simplement pas.
|
||||
@ -992,6 +1053,10 @@ impl AgentProfile {
|
||||
(Some(StructuredAdapter::Codex), Some(McpConfigStrategy::TomlConfigHome { .. })) => {
|
||||
true
|
||||
}
|
||||
(
|
||||
Some(StructuredAdapter::OpenCode),
|
||||
Some(McpConfigStrategy::OpenCodeConfig { target }),
|
||||
) => target == "opencode.json",
|
||||
(Some(StructuredAdapter::OpenAiCompatible), _) => false,
|
||||
_ => false,
|
||||
}
|
||||
@ -1069,6 +1134,36 @@ mod mcp_tests {
|
||||
assert_eq!(back, adapter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_adapter_is_distinct_and_materialises_open_code_config_only() {
|
||||
let adapter = StructuredAdapter::OpenCode;
|
||||
assert_eq!(adapter.provider_key(), "opencode");
|
||||
let json = serde_json::to_string(&adapter).expect("serialise");
|
||||
assert_eq!(json, "\"openCode\"");
|
||||
let back: StructuredAdapter = serde_json::from_str(&json).expect("deserialise");
|
||||
assert_eq!(back, adapter);
|
||||
|
||||
let profile = profile_without_mcp()
|
||||
.with_structured_adapter(StructuredAdapter::OpenCode)
|
||||
.with_opencode(OpenCodeConfig::new(Some("ollama/qwen3-coder:30b".to_owned())).unwrap())
|
||||
.with_mcp(McpCapability::new(
|
||||
McpConfigStrategy::open_code_config("opencode.json").unwrap(),
|
||||
McpTransport::Stdio,
|
||||
));
|
||||
assert!(profile.materializes_idea_bridge());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_stays_non_mcp_native_even_with_mcp_declared() {
|
||||
let profile = profile_without_mcp()
|
||||
.with_structured_adapter(StructuredAdapter::OpenAiCompatible)
|
||||
.with_mcp(McpCapability::new(
|
||||
McpConfigStrategy::open_code_config("opencode.json").unwrap(),
|
||||
McpTransport::Stdio,
|
||||
));
|
||||
assert!(!profile.materializes_idea_bridge());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_chat_config_validates_without_io_and_round_trips() {
|
||||
let config = HttpChatConfig::new(
|
||||
|
||||
Reference in New Issue
Block a user