feat(backend): catalogue de providers OpenCode dynamique + provider personnalisé (#92)
Le catalogue de providers OpenCode lit désormais le cache local ~/.cache/opencode/models.json pour refléter les providers réellement disponibles, avec repli garanti sur le catalogue statique en cas d'absence ou d'erreur de lecture du cache. Ajout d'un champ additif `custom` sur OpenCodeProviderConfig pour permettre à l'utilisateur de déclarer un provider hors catalogue (id + clé API en saisie libre). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -381,12 +381,20 @@ pub struct OpenCodeProviderConfig {
|
||||
/// couche application autorisée à manipuler le littéral (mint du `SecretRef` +
|
||||
/// écriture dans le `SecretStore`) construit cette référence.
|
||||
pub api_key_ref: crate::ports::SecretRef,
|
||||
/// Configuration additive d'un provider **non enregistré** dans le catalogue
|
||||
/// OpenCode (endpoint OpenAI-compatible arbitraire). `None` = provider connu
|
||||
/// du registre OpenCode (comportement historique inchangé) ; `Some` fait
|
||||
/// émettre le bloc `npm`/`options.baseURL`/`models` en plus de `apiKey` côté
|
||||
/// rendu JSON (voir `opencode_provider_config_json`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub custom: Option<CustomProviderConfig>,
|
||||
}
|
||||
|
||||
impl OpenCodeProviderConfig {
|
||||
/// Construit une configuration validée (parse-don't-validate, comme
|
||||
/// [`OpenCodeConfig::new`]). Ne prend jamais de clé littérale : seule une
|
||||
/// référence déjà mintée par l'application est acceptée.
|
||||
/// référence déjà mintée par l'application est acceptée. `custom` est `None`
|
||||
/// (provider connu) ; voir [`Self::with_custom`] pour un provider personnalisé.
|
||||
///
|
||||
/// # Errors
|
||||
/// Renvoie [`DomainError::EmptyField`] si `provider_id` ou `model` est vide.
|
||||
@ -403,6 +411,56 @@ impl OpenCodeProviderConfig {
|
||||
provider_id,
|
||||
model,
|
||||
api_key_ref,
|
||||
custom: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attache une configuration de provider personnalisé (builder, additif).
|
||||
#[must_use]
|
||||
pub fn with_custom(mut self, custom: CustomProviderConfig) -> Self {
|
||||
self.custom = Some(custom);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration additive d'un provider OpenCode **personnalisé** (endpoint
|
||||
/// OpenAI-compatible arbitraire, hors catalogue OpenCode), portée par
|
||||
/// [`OpenCodeProviderConfig::custom`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomProviderConfig {
|
||||
/// Paquet npm du SDK AI utilisé pour parler à ce provider (ex.
|
||||
/// `"@ai-sdk/openai-compatible"`).
|
||||
pub npm: String,
|
||||
/// URL de base de l'endpoint OpenAI-compatible.
|
||||
pub base_url: String,
|
||||
/// Libellé optionnel affiché pour le modèle (défaut : l'id du modèle).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
impl CustomProviderConfig {
|
||||
/// Construit une configuration validée (parse-don't-validate).
|
||||
///
|
||||
/// # Errors
|
||||
/// Renvoie [`DomainError::EmptyField`] si `npm`, `base_url`, ou un
|
||||
/// `display_name` fourni non vide après trim, est vide.
|
||||
pub fn new(
|
||||
npm: impl Into<String>,
|
||||
base_url: impl Into<String>,
|
||||
display_name: Option<String>,
|
||||
) -> Result<Self, DomainError> {
|
||||
let npm = npm.into();
|
||||
let base_url = base_url.into();
|
||||
crate::validation::non_empty(&npm, "opencodeProvider.custom.npm")?;
|
||||
crate::validation::non_empty(&base_url, "opencodeProvider.custom.baseUrl")?;
|
||||
if let Some(name) = &display_name {
|
||||
crate::validation::non_empty(name, "opencodeProvider.custom.displayName")?;
|
||||
}
|
||||
Ok(Self {
|
||||
npm,
|
||||
base_url,
|
||||
display_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user