feat: catalogue dynamique modèles Codex/Claude avec compatibilité CLI locale
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
This commit is contained in:
216
crates/domain/src/model_catalogue.rs
Normal file
216
crates/domain/src/model_catalogue.rs
Normal file
@ -0,0 +1,216 @@
|
||||
//! Pure model-catalogue compatibility types.
|
||||
|
||||
use core::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::profile::StructuredAdapter;
|
||||
|
||||
/// Parsed CLI version used for local compatibility checks.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliVersion {
|
||||
/// Original version string reported by the CLI.
|
||||
pub raw: String,
|
||||
parts: Vec<u64>,
|
||||
}
|
||||
|
||||
impl CliVersion {
|
||||
/// Parses a version from a string containing at least one digit.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`ModelCatalogueError::InvalidVersion`] when no numeric version
|
||||
/// segment can be found.
|
||||
pub fn parse(raw: impl Into<String>) -> Result<Self, ModelCatalogueError> {
|
||||
let raw = raw.into();
|
||||
let start = raw
|
||||
.char_indices()
|
||||
.find_map(|(idx, ch)| ch.is_ascii_digit().then_some(idx))
|
||||
.ok_or_else(|| ModelCatalogueError::InvalidVersion(raw.clone()))?;
|
||||
let version = raw[start..]
|
||||
.chars()
|
||||
.take_while(|ch| ch.is_ascii_digit() || *ch == '.')
|
||||
.collect::<String>();
|
||||
let parts = version
|
||||
.split('.')
|
||||
.filter(|part| !part.is_empty())
|
||||
.map(str::parse::<u64>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| ModelCatalogueError::InvalidVersion(raw.clone()))?;
|
||||
if parts.is_empty() {
|
||||
return Err(ModelCatalogueError::InvalidVersion(raw));
|
||||
}
|
||||
Ok(Self { raw, parts })
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for CliVersion {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for CliVersion {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
let max_len = self.parts.len().max(other.parts.len());
|
||||
for idx in 0..max_len {
|
||||
match self
|
||||
.parts
|
||||
.get(idx)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.cmp(&other.parts.get(idx).copied().unwrap_or(0))
|
||||
{
|
||||
Ordering::Equal => {}
|
||||
ordering => return ordering,
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
/// Compatibility state between a local CLI version and a model id.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ModelCompatibility {
|
||||
/// The known minimum CLI version is satisfied.
|
||||
Compatible,
|
||||
/// The CLI version is absent, or the model is not covered by the matrix.
|
||||
Unknown,
|
||||
/// The model is covered by the matrix but appears newer than the local CLI.
|
||||
LikelyTooRecent,
|
||||
}
|
||||
|
||||
/// Origin of a model-catalogue entry.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ModelCatalogSource {
|
||||
/// Curated static seed maintained by IdeA.
|
||||
Catalogue,
|
||||
/// Best-effort provider API discovery.
|
||||
Provider,
|
||||
}
|
||||
|
||||
/// Matrix mapping adapter/model ids to their minimum known CLI version.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompatibilityMatrix {
|
||||
/// Matrix schema/data version.
|
||||
pub version: u32,
|
||||
/// Claude Code entries keyed by model id.
|
||||
#[serde(default)]
|
||||
pub claude: HashMap<String, String>,
|
||||
/// Codex CLI entries keyed by model id.
|
||||
#[serde(default)]
|
||||
pub codex: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl CompatibilityMatrix {
|
||||
/// Looks up the minimum CLI version for an adapter/model pair.
|
||||
#[must_use]
|
||||
pub fn minimum_version(&self, adapter: StructuredAdapter, model_id: &str) -> Option<&str> {
|
||||
match adapter {
|
||||
StructuredAdapter::Claude => self.claude.get(model_id).map(String::as_str),
|
||||
StructuredAdapter::Codex => self.codex.get(model_id).map(String::as_str),
|
||||
StructuredAdapter::OpenCode | StructuredAdapter::OpenAiCompatible => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates local compatibility using only pure matrix data.
|
||||
#[must_use]
|
||||
pub fn evaluate_compatibility(
|
||||
matrix: &CompatibilityMatrix,
|
||||
adapter: StructuredAdapter,
|
||||
model_id: &str,
|
||||
cli_version: Option<&CliVersion>,
|
||||
) -> ModelCompatibility {
|
||||
let Some(cli_version) = cli_version else {
|
||||
return ModelCompatibility::Unknown;
|
||||
};
|
||||
let Some(minimum) = matrix.minimum_version(adapter, model_id) else {
|
||||
return ModelCompatibility::Unknown;
|
||||
};
|
||||
let Ok(minimum) = CliVersion::parse(minimum.to_owned()) else {
|
||||
return ModelCompatibility::Unknown;
|
||||
};
|
||||
if &minimum <= cli_version {
|
||||
ModelCompatibility::Compatible
|
||||
} else {
|
||||
ModelCompatibility::LikelyTooRecent
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from pure model-catalogue parsing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ModelCatalogueError {
|
||||
/// Version strings must contain at least one numeric segment.
|
||||
#[error("invalid CLI version: {0}")]
|
||||
InvalidVersion(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn matrix() -> CompatibilityMatrix {
|
||||
CompatibilityMatrix {
|
||||
version: 1,
|
||||
claude: HashMap::from([("claude-sonnet-5".to_owned(), "1.2.0".to_owned())]),
|
||||
codex: HashMap::from([("gpt-5-codex".to_owned(), "0.44.0".to_owned())]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_versions_compare_by_numeric_parts() {
|
||||
assert!(CliVersion::parse("codex 0.10.0").unwrap() > CliVersion::parse("0.9.9").unwrap());
|
||||
assert_eq!(
|
||||
CliVersion::parse("1.2")
|
||||
.unwrap()
|
||||
.cmp(&CliVersion::parse("1.2.0").unwrap()),
|
||||
Ordering::Equal
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_is_unknown_without_version_or_matrix_entry() {
|
||||
let matrix = matrix();
|
||||
assert_eq!(
|
||||
evaluate_compatibility(&matrix, StructuredAdapter::Codex, "gpt-5-codex", None),
|
||||
ModelCompatibility::Unknown
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate_compatibility(
|
||||
&matrix,
|
||||
StructuredAdapter::Codex,
|
||||
"future-model",
|
||||
Some(&CliVersion::parse("999.0.0").unwrap())
|
||||
),
|
||||
ModelCompatibility::Unknown
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_detects_supported_and_too_recent_models() {
|
||||
let matrix = matrix();
|
||||
assert_eq!(
|
||||
evaluate_compatibility(
|
||||
&matrix,
|
||||
StructuredAdapter::Codex,
|
||||
"gpt-5-codex",
|
||||
Some(&CliVersion::parse("0.44.0").unwrap())
|
||||
),
|
||||
ModelCompatibility::Compatible
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate_compatibility(
|
||||
&matrix,
|
||||
StructuredAdapter::Claude,
|
||||
"claude-sonnet-5",
|
||||
Some(&CliVersion::parse("1.1.9").unwrap())
|
||||
),
|
||||
ModelCompatibility::LikelyTooRecent
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user