From ca70ec75f4acc061fbbf6e6c1b0dfadcc41f9e56 Mon Sep 17 00:00:00 2001
From: Blomios
Date: Sun, 26 Jul 2026 16:09:10 +0200
Subject: [PATCH] =?UTF-8?q?feat:=20catalogue=20dynamique=20mod=C3=A8les=20?=
=?UTF-8?q?Codex/Claude=20avec=20compatibilit=C3=A9=20CLI=20locale?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ajout du catalogue enrichi pour les modèles Codex et Claude avec:
- Compatibilité estimée avec la version CLI locale détectée
- Source d'origine (catalogue/Provider) pour chaque entrée
- Support du catalogue Provider API externe
- Matrice de compatibilité embarquée dans l'application
Frontend:
- UI de configuration des modèles avec affichage des états de compatibilité
- Suggestions dynamiques avec badges de compatibilité
- Messages d'aide contextuels (compatible/unknown/likelyTooRecent)
- Alertes non-bloquantes pour les modèles trop récents
- Gestion des échecs de catalogue avec saisie manuelle conservée
Backend:
- Ports CliVersionReader, ProviderModelCatalogue, CompatibilityMatrixSource
- Implémentations: ProcessCliVersionReader, HttpProviderModelCatalogue, EmbeddedCompatibilityMatrix
- Enrichissement des DTOs avec compatibility, cli_version, warnings
- Tests unitaires complets pour le resolver de catalogue
---
.ideai/memory/MEMORY.md | 1 +
.../memory/model-catalogue-compat-cadrage.md | 25 ++
crates/app-tauri/src/commands.rs | 8 +-
crates/app-tauri/tests/dto_profiles.rs | 35 +-
.../application/src/agent/model_catalogue.rs | 369 ++++++++++++++++--
crates/application/src/lib.rs | 10 +-
crates/application/tests/profile_usecases.rs | 16 +-
crates/backend/src/dto.rs | 31 +-
crates/backend/src/lib.rs | 44 ++-
crates/domain/src/lib.rs | 27 +-
crates/domain/src/model_catalogue.rs | 216 ++++++++++
crates/domain/src/ports.rs | 34 +-
crates/infrastructure/src/lib.rs | 4 +
crates/infrastructure/src/model_catalogue.rs | 331 ++++++++++++++++
.../src/model_compatibility_matrix.json | 13 +
crates/web-server/src/lib.rs | 38 +-
.../adapters/http/requestResponseGateways.ts | 11 +-
frontend/src/adapters/mock/index.ts | 29 +-
frontend/src/adapters/profile.ts | 11 +-
frontend/src/adapters/profileCatalog.test.ts | 56 +++
frontend/src/adapters/profileCatalog.ts | 63 +++
frontend/src/domain/index.ts | 22 ++
.../first-run/ProfilesSettings.test.tsx | 55 ++-
.../features/first-run/ProfilesSettings.tsx | 290 ++++++++++++--
frontend/src/ports/index.ts | 10 +-
25 files changed, 1582 insertions(+), 167 deletions(-)
create mode 100644 .ideai/memory/model-catalogue-compat-cadrage.md
create mode 100644 crates/domain/src/model_catalogue.rs
create mode 100644 crates/infrastructure/src/model_catalogue.rs
create mode 100644 crates/infrastructure/src/model_compatibility_matrix.json
create mode 100644 frontend/src/adapters/profileCatalog.test.ts
create mode 100644 frontend/src/adapters/profileCatalog.ts
diff --git a/.ideai/memory/MEMORY.md b/.ideai/memory/MEMORY.md
index 9895848..5d8a66b 100644
--- a/.ideai/memory/MEMORY.md
+++ b/.ideai/memory/MEMORY.md
@@ -71,3 +71,4 @@
- [ticket103-network-permission-ux-surface](ticket103-network-permission-ux-surface.md) — Stable UX convention for agent network permissions in IdeA.
- [codex-network-access-config-fix](codex-network-access-config-fix.md) — memory note codex-network-access-config-fix
- [multi-profile-codex-claude-model-catalogue-scoping](multi-profile-codex-claude-model-catalogue-scoping.md) — memory note multi-profile-codex-claude-model-catalogue-scoping
+- [model-catalogue-compat-cadrage](model-catalogue-compat-cadrage.md) — Frontières hexagonales, ports, DTO, fallback et matrice de compatibilité versionnée pour l'évolution du catalogue de modèles des profils structurés Codex/Claude.
diff --git a/.ideai/memory/model-catalogue-compat-cadrage.md b/.ideai/memory/model-catalogue-compat-cadrage.md
new file mode 100644
index 0000000..6aa85c9
--- /dev/null
+++ b/.ideai/memory/model-catalogue-compat-cadrage.md
@@ -0,0 +1,25 @@
+---
+name: model-catalogue-compat-cadrage
+description: Frontières hexagonales, ports, DTO, fallback et matrice de compatibilité versionnée pour l'évolution du catalogue de modèles des profils structurés Codex/Claude.
+metadata:
+ type: reference
+---
+Évolution de `ListClaudeModels`/`ListCodexModels` (catalogue statique `application/src/agent/model_catalogue.rs`) vers un catalogue enrichi par compatibilité CLI.
+
+**Décisions tranchées :**
+- JAMAIS scraper les TUI `/model`, JAMAIS exécuter les CLIs pour énumérer les modèles. Seule exécution CLI autorisée : ` --version` (pattern existant `infrastructure/runtime::detection_spec` + port `ProcessSpawner`). Saisie libre toujours ouverte.
+- 3 sources non bloquantes à dégradation indépendante : API provider `/v1/models` (best-effort, uniquement si clé env/SecretStore présente, sinon skip), catalogue statique seed, matrice de compat.
+
+**Hexagonal :**
+- Domaine (pur) : VO `CliVersion` (Ord), enum `ModelCompatibility {Compatible|Unknown|LikelyTooRecent}` (miroir des 3 états produit), VO `CompatibilityMatrix` (forme seule), fonction pure `evaluate_compatibility(matrix, adapter, model_id, Option)` — version None ⇒ Unknown, modèle absent ⇒ Unknown, min<=ver ⇒ Compatible, min>ver ⇒ LikelyTooRecent.
+- Nouveaux ports : `CliVersionReader`, `ProviderModelCatalogue` (Ok(vec![]) si pas de clé), `CompatibilityMatrixSource` (infaillible).
+- Application : use case unique `ResolveModelCatalogue{adapter}` async, jamais de hard-error sur échec source (warnings + fallback). `ListClaude/CodexModels` deviennent des façades.
+- Infra : `ProcessCliVersionReader`, `HttpProviderModelCatalogue` (reqwest), `EmbeddedCompatibilityMatrix`.
+
+**Matrice de compat = DONNÉE, pas code** : JSON versionné maintenu dans IdeA, bundlé via `include_str!` (seed infaillible) + override optionnel `app_data_dir/IdeA/model-compat.json`. Ajouter un modèle = éditer le JSON, zéro code (Open/Closed).
+
+**DTO (rupture front)** : `ProfileModelCatalogDto` passe de `transparent Vec` à `{ models:[{...,compatibility,source}], cliVersion:string|null, warnings:string[] }`. Répercuter ports TS + 2 adapters + mock + ProfilesSettings.
+
+**Découpage** : B1 domaine pur, B2 use case ports mockés, B3 infra ; F1 contrat, F2 ProfilesSettings 3 badges. UX passe avant F2 (libellés des 3 états, warnings, cliVersion null).
+
+**Point ouvert produit** : récup clé provider — proposé best-effort sur clé env/SecretStore existante, pas de prompt dédié.
\ No newline at end of file
diff --git a/crates/app-tauri/src/commands.rs b/crates/app-tauri/src/commands.rs
index c14d689..82fe758 100644
--- a/crates/app-tauri/src/commands.rs
+++ b/crates/app-tauri/src/commands.rs
@@ -1189,20 +1189,20 @@ pub async fn list_opencode_providers(
Ok(state.list_opencode_providers.execute().into())
}
-/// `list_claude_models` — static curated Claude model catalogue.
+/// `list_claude_models` — enriched Claude model catalogue.
#[tauri::command]
pub async fn list_claude_models(
state: State<'_, AppState>,
) -> Result {
- Ok(state.list_claude_models.execute().into())
+ Ok(state.list_claude_models.execute().await.into())
}
-/// `list_codex_models` — static curated Codex model catalogue.
+/// `list_codex_models` — enriched Codex model catalogue.
#[tauri::command]
pub async fn list_codex_models(
state: State<'_, AppState>,
) -> Result {
- Ok(state.list_codex_models.execute().into())
+ Ok(state.list_codex_models.execute().await.into())
}
/// `save_opencode_provider_profile` — create or replace an OpenCode profile
diff --git a/crates/app-tauri/tests/dto_profiles.rs b/crates/app-tauri/tests/dto_profiles.rs
index 81cb35e..7d583e2 100644
--- a/crates/app-tauri/tests/dto_profiles.rs
+++ b/crates/app-tauri/tests/dto_profiles.rs
@@ -141,21 +141,30 @@ fn clone_profile_from_seed_request_deserialises_camelcase_overrides() {
#[test]
fn profile_model_catalogue_dto_serialises_searchable_camelcase_entries() {
- let dto = ProfileModelCatalogDto(vec![app_tauri_lib::dto::ProfileModelCatalogEntryDto {
- adapter: StructuredAdapter::Codex,
- model_id: "gpt-5-codex".to_owned(),
- display_name: "GPT-5 Codex".to_owned(),
- aliases: vec!["codex".to_owned()],
- recommended: true,
- }]);
+ let dto = ProfileModelCatalogDto {
+ models: vec![app_tauri_lib::dto::ProfileModelCatalogEntryDto {
+ adapter: StructuredAdapter::Codex,
+ model_id: "gpt-5-codex".to_owned(),
+ display_name: "GPT-5 Codex".to_owned(),
+ aliases: vec!["codex".to_owned()],
+ recommended: true,
+ compatibility: domain::ModelCompatibility::Compatible,
+ source: domain::ModelCatalogSource::Catalogue,
+ }],
+ cli_version: Some("0.45.1".to_owned()),
+ warnings: vec!["provider unavailable".to_owned()],
+ };
let value = serde_json::to_value(&dto).unwrap();
- let arr = value.as_array().expect("transparent array");
- assert_eq!(arr[0]["adapter"], "codex");
- assert_eq!(arr[0]["modelId"], "gpt-5-codex");
- assert_eq!(arr[0]["displayName"], "GPT-5 Codex");
- assert_eq!(arr[0]["aliases"], json!(["codex"]));
- assert_eq!(arr[0]["recommended"], true);
+ assert_eq!(value["cliVersion"], "0.45.1");
+ assert_eq!(value["warnings"], json!(["provider unavailable"]));
+ assert_eq!(value["models"][0]["adapter"], "codex");
+ assert_eq!(value["models"][0]["modelId"], "gpt-5-codex");
+ assert_eq!(value["models"][0]["displayName"], "GPT-5 Codex");
+ assert_eq!(value["models"][0]["aliases"], json!(["codex"]));
+ assert_eq!(value["models"][0]["recommended"], true);
+ assert_eq!(value["models"][0]["compatibility"], "compatible");
+ assert_eq!(value["models"][0]["source"], "catalogue");
}
#[test]
diff --git a/crates/application/src/agent/model_catalogue.rs b/crates/application/src/agent/model_catalogue.rs
index f4d2fb1..e5399b8 100644
--- a/crates/application/src/agent/model_catalogue.rs
+++ b/crates/application/src/agent/model_catalogue.rs
@@ -1,10 +1,17 @@
-//! Static curated model catalogues for structured Claude/Codex profiles.
+//! Curated and best-effort model catalogues for structured Claude/Codex profiles.
//!
-//! The CLIs do not expose a stable machine-readable model catalogue. These lists
-//! are therefore intentionally small, static and infallible; the UI must still
-//! keep manual entry as a fallback for models not listed here.
+//! The CLIs do not expose a stable machine-readable model catalogue and must not
+//! be asked to enumerate models. The only local probe allowed here is
+//! ` --version`; provider APIs are optional and failures degrade to the
+//! static seed plus warnings.
+use std::collections::BTreeSet;
+use std::sync::Arc;
+
+use domain::model_catalogue::{evaluate_compatibility, CliVersion};
+use domain::ports::{CliVersionReader, CompatibilityMatrixSource, ProviderModelCatalogue};
use domain::profile::StructuredAdapter;
+use domain::{ModelCatalogSource, ModelCompatibility};
/// One searchable model entry for a structured profile adapter.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -19,6 +26,10 @@ pub struct ProfileModelCatalogEntry {
pub aliases: Vec,
/// Whether this entry is the conservative default suggestion.
pub recommended: bool,
+ /// Compatibility state against the locally detected CLI version.
+ pub compatibility: ModelCompatibility,
+ /// Source that contributed the model entry.
+ pub source: ModelCatalogSource,
}
fn entry(
@@ -34,6 +45,20 @@ fn entry(
display_name: display_name.to_owned(),
aliases: aliases.iter().map(|alias| (*alias).to_owned()).collect(),
recommended,
+ compatibility: ModelCompatibility::Unknown,
+ source: ModelCatalogSource::Catalogue,
+ }
+}
+
+fn provider_entry(adapter: StructuredAdapter, model_id: String) -> ProfileModelCatalogEntry {
+ ProfileModelCatalogEntry {
+ adapter,
+ display_name: model_id.clone(),
+ model_id,
+ aliases: Vec::new(),
+ recommended: false,
+ compatibility: ModelCompatibility::Unknown,
+ source: ModelCatalogSource::Provider,
}
}
@@ -93,73 +118,254 @@ pub fn codex_model_catalogue() -> Vec {
]
}
-/// Use case exposing the static Claude model catalogue.
-pub struct ListClaudeModels;
+/// Output of structured model-catalogue resolution.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ListModelsOutput {
+ /// The catalogue entries.
+ pub models: Vec,
+ /// Best-effort local CLI version.
+ pub cli_version: Option,
+ /// Non-fatal fallback/degradation warnings.
+ pub warnings: Vec,
+}
/// Output of [`ListClaudeModels::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListClaudeModelsOutput {
/// The catalogue entries.
pub models: Vec,
+ /// Best-effort local CLI version.
+ pub cli_version: Option,
+ /// Non-fatal fallback/degradation warnings.
+ pub warnings: Vec,
}
-impl ListClaudeModels {
- /// Builds the use case (stateless, no ports to inject).
- #[must_use]
- pub const fn new() -> Self {
- Self
- }
-
- /// Lists curated Claude models. Infallible.
- #[must_use]
- pub fn execute(&self) -> ListClaudeModelsOutput {
- ListClaudeModelsOutput {
- models: claude_model_catalogue(),
+impl From for ListClaudeModelsOutput {
+ fn from(out: ListModelsOutput) -> Self {
+ Self {
+ models: out.models,
+ cli_version: out.cli_version,
+ warnings: out.warnings,
}
}
}
-impl Default for ListClaudeModels {
- fn default() -> Self {
- Self::new()
- }
-}
-
-/// Use case exposing the static Codex model catalogue.
-pub struct ListCodexModels;
-
/// Output of [`ListCodexModels::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListCodexModelsOutput {
/// The catalogue entries.
pub models: Vec,
+ /// Best-effort local CLI version.
+ pub cli_version: Option,
+ /// Non-fatal fallback/degradation warnings.
+ pub warnings: Vec,
}
-impl ListCodexModels {
- /// Builds the use case (stateless, no ports to inject).
- #[must_use]
- pub const fn new() -> Self {
- Self
- }
-
- /// Lists curated Codex models. Infallible.
- #[must_use]
- pub fn execute(&self) -> ListCodexModelsOutput {
- ListCodexModelsOutput {
- models: codex_model_catalogue(),
+impl From for ListCodexModelsOutput {
+ fn from(out: ListModelsOutput) -> Self {
+ Self {
+ models: out.models,
+ cli_version: out.cli_version,
+ warnings: out.warnings,
}
}
}
-impl Default for ListCodexModels {
- fn default() -> Self {
- Self::new()
+/// Use case resolving an enriched structured model catalogue.
+pub struct ResolveModelCatalogue {
+ cli_versions: Arc,
+ provider_catalogue: Arc,
+ matrix_source: Arc,
+}
+
+impl ResolveModelCatalogue {
+ /// Builds the use case from hexagonal ports.
+ #[must_use]
+ pub fn new(
+ cli_versions: Arc,
+ provider_catalogue: Arc,
+ matrix_source: Arc,
+ ) -> Self {
+ Self {
+ cli_versions,
+ provider_catalogue,
+ matrix_source,
+ }
+ }
+
+ /// Resolves models for one structured adapter. Infallible by design.
+ pub async fn execute(&self, adapter: StructuredAdapter) -> ListModelsOutput {
+ let mut warnings = Vec::new();
+ let (matrix, matrix_warnings) = self.matrix_source.compatibility_matrix();
+ warnings.extend(matrix_warnings);
+
+ let cli_version = match self.cli_versions.read_cli_version(adapter).await {
+ Ok(version) => version,
+ Err(warning) => {
+ warnings.push(warning);
+ None
+ }
+ };
+
+ let mut models = match adapter {
+ StructuredAdapter::Claude => claude_model_catalogue(),
+ StructuredAdapter::Codex => codex_model_catalogue(),
+ StructuredAdapter::OpenCode | StructuredAdapter::OpenAiCompatible => Vec::new(),
+ };
+
+ match self.provider_catalogue.list_provider_models(adapter).await {
+ Ok(provider_models) => {
+ let existing = models
+ .iter()
+ .map(|model| model.model_id.clone())
+ .collect::>();
+ models.extend(
+ provider_models
+ .into_iter()
+ .filter(|model_id| !existing.contains(model_id))
+ .map(|model_id| provider_entry(adapter, model_id)),
+ );
+ }
+ Err(warning) => warnings.push(warning),
+ }
+
+ for model in &mut models {
+ model.compatibility =
+ evaluate_compatibility(&matrix, adapter, &model.model_id, cli_version.as_ref());
+ }
+
+ models.sort_by(|a, b| {
+ b.recommended
+ .cmp(&a.recommended)
+ .then_with(|| a.display_name.cmp(&b.display_name))
+ .then_with(|| a.model_id.cmp(&b.model_id))
+ });
+
+ ListModelsOutput {
+ models,
+ cli_version,
+ warnings,
+ }
+ }
+}
+
+/// Use case exposing the Claude model catalogue.
+pub struct ListClaudeModels {
+ resolver: ResolveModelCatalogue,
+}
+
+impl ListClaudeModels {
+ /// Builds the use case.
+ #[must_use]
+ pub fn new(
+ cli_versions: Arc,
+ provider_catalogue: Arc,
+ matrix_source: Arc,
+ ) -> Self {
+ Self {
+ resolver: ResolveModelCatalogue::new(cli_versions, provider_catalogue, matrix_source),
+ }
+ }
+
+ /// Lists Claude models. Infallible.
+ pub async fn execute(&self) -> ListClaudeModelsOutput {
+ self.resolver
+ .execute(StructuredAdapter::Claude)
+ .await
+ .into()
+ }
+}
+
+/// Use case exposing the Codex model catalogue.
+pub struct ListCodexModels {
+ resolver: ResolveModelCatalogue,
+}
+
+impl ListCodexModels {
+ /// Builds the use case.
+ #[must_use]
+ pub fn new(
+ cli_versions: Arc,
+ provider_catalogue: Arc,
+ matrix_source: Arc,
+ ) -> Self {
+ Self {
+ resolver: ResolveModelCatalogue::new(cli_versions, provider_catalogue, matrix_source),
+ }
+ }
+
+ /// Lists Codex models. Infallible.
+ pub async fn execute(&self) -> ListCodexModelsOutput {
+ self.resolver.execute(StructuredAdapter::Codex).await.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
+ use async_trait::async_trait;
+ use domain::{CliVersion, CompatibilityMatrix};
+ use std::collections::HashMap;
+
+ struct FakeCliVersionReader(Option, String>>);
+
+ #[async_trait]
+ impl CliVersionReader for FakeCliVersionReader {
+ async fn read_cli_version(
+ &self,
+ _adapter: StructuredAdapter,
+ ) -> Result
)}
-
{visibleProfiles.length === 0 ? (
@@ -296,25 +509,42 @@ export function ProfilesSettings() {
/>
-
+ ) : (
+
+ )}
+ {saveWarnings[saved.id] && (
+
+ {saveWarnings[saved.id]}
+
+ )}
diff --git a/frontend/src/ports/index.ts b/frontend/src/ports/index.ts
index cc4815f..4a1af1c 100644
--- a/frontend/src/ports/index.ts
+++ b/frontend/src/ports/index.ts
@@ -37,7 +37,7 @@ import type {
McpToolPolicy,
OpenCodeConfig,
OpenCodeProviderCatalogEntry,
- ProfileModelCatalogEntry,
+ ProfileModelCatalog,
EffectivePermissions,
PairedDevice,
PairingCode,
@@ -670,10 +670,10 @@ export interface ProfileGateway {
* Used by Settings duplication for Codex/Claude/OpenCode identity copies.
*/
cloneProfileFromSeed(input: CloneProfileFromSeedInput): Promise;
- /** Curated Claude Code model catalogue. Manual model entry remains supported. */
- listClaudeModels(): Promise;
- /** Curated Codex CLI model catalogue. Manual model entry remains supported. */
- listCodexModels(): Promise;
+ /** Enriched Claude Code model catalogue. Manual model entry remains supported. */
+ listClaudeModels(): Promise;
+ /** Enriched Codex CLI model catalogue. Manual model entry remains supported. */
+ listCodexModels(): Promise;
/** Persists the batch of chosen profiles, closing the first run. */
configureProfiles(profiles: AgentProfile[]): Promise;
/**