Files
IdeA/.ideai/memory/ticket98-opencode-modelsdev-cache-seed.md

7.8 KiB
Raw Blame History

name, description, metadata
name description metadata
ticket98-opencode-modelsdev-cache-seed memory note ticket98-opencode-modelsdev-cache-seed
type
project

Ticket #98 — Fix spawn OpenCode : seed du cache models.dev isolé

Symptôme

Wizard profil OpenCode first-run : provider catalogue cloud (ex: zai/ZAI Code) + modèle choisi → après sauvegarde l'agent tourne en glm-5.2 quel que soit le modèle. Persistance correcte (profiles.json conserve opencodeProvider.model). glm-5.2 est le fallback interne d'OpenCode, absent du code IdeA.

Cause racine (confirmée + affinée par Architect)

Asymétrie picker↔spawn, pas seulement « pas de bloc models » :

  1. Picker (provider_catalogue.rs:79-84,150-153) : lit le cache models.dev via opencode_models_cache_path() qui résout le vrai cache hôte (XDG_CACHE_HOME ?? ~/.cache). Permet d'afficher zai + ses modèles.
  2. Spawn (lifecycle.rs:2386,2400-2407 ; assistant/mod.rs:272,277-285) : IdeA ré-isole XDG_CACHE_HOME.opencode/cache vide sous run_dir. OpenCode ne voit plus le cache.
  3. Rendu (opencode_provider_config_json, lifecycle.rs:2795-2821 / assistant/mod.rs:446-466) : pour config.custom == None (défaut pour zai), IdeA n'émet que apiKey + model: "zai/<m>", sans bloc models. Correct uniquement pour les 3 built-ins hardcodés dans le binaire OpenCode (anthropic, openai, openrouter).

→ OpenCode reçoit model: "zai/<m>", ne reconnaît pas zai comme built-in, ne trouve pas le cache models.dev (isolé vide) → fallback silencieux glm-5.2.

  • Local/custom marchent : émettent un bloc models auto-suffisant (assistant/mod.rs:364-381, :447-460).
  • 3 built-ins marchent : hardcodés dans OpenCode.
  • Bug ne touche que les providers connus d'OpenCode exclusivement via models.dev.

Approche figée : B2 — Semer le cache models.dev isolé depuis le cache hôte

Après création du XDG_CACHE_HOME isolé, copier best-effort le fichier cache models.dev hôte (opencode_models_cache_path()<isolated_cache>/opencode/models.json) via le port FileSystem. Lecture hôte = std::fs (identique au picker) ; écriture dans la home isolée.

Pourquoi pas les autres :

  • (A) Émettre un bloc models+npm+baseURL pour les catalogue → écartée en v1 : IdeA devrait capturer le bon npm AI-SDK par provider depuis models.dev (@ai-sdk/anthropic vs openai-compatible…) — dupliquerait la connaissance du registry OpenCode (violation OCP), risque de régression built-ins. Gardée en escalade si B2 défait par refresh.
  • (B1) Pointer vers le vrai cache hôte (ne plus isoler) → écartée : casse l'isolation en écriture (pollution + race entre sessions).
  • (C) Bundler une copie statique models.dev dans IdeA → écartée : staleness + diverge picker/spawn.

Auto-cohérence B2 : la précondition du bug (cache hôte présent au picker) garantit la précondition du fix (fichier à copier au spawn). Cache hôte absent → picker ne montre que les 3 built-ins → bug non atteint → fix non requis.

Contrat figé

Ports / DTO

  • Aucun nouveau port / entité domaine / DTO modifié. Réutilise le port FileSystem déjà injecté sur le chemin spawn. Lecture source = std::fs hôte (mécanisme identique au picker).
  • Rendre publique opencode_models_cache_path() (source de vérité unique — encode le workaround du bug upstream #8235 ; ne pas re-dériver).

Fichiers (périmètre DevBackend)

  • crates/application/src/agent/provider_catalogue.rs — exposer opencode_models_cache_path() en pub (ou ajouter pub fn read_models_dev_cache_bytes() -> Option<Vec<u8>>).
  • crates/application/src/agent/mod.rs — re-export.
  • crates/application/src/agent/lifecycle.rs — branche apply_mcp_config (≈2382-2408) : après create_dir_all du cache + création xdg_cache, semer <xdg_cache>/opencode/models.json depuis le cache hôte, best-effort. Branche commune opencode + opencodeProvider.
  • crates/infrastructure/src/assistant/mod.rs — branche miroir (≈268-285) : même appel.
  • Factoriser : application::agent::seed_opencode_models_cache(fs: &dyn FileSystem, isolated_cache_dir: &str) appelée par les deux sites. Partie pure testable seed_from_bytes(fs, dest, src_bytes) ; wrapper impur std::fs::read fin.

Invariants (stricts)

  1. Isolation préservée en écriture : HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME restent sous run_dir. Aucune var d'env repointée vers l'hôte. Seul le contenu du cache isolé est semé (lecture seule).
  2. Best-effort, n'échoue jamais le launch : source absente/illisible ou erreur FS → pas d'échec du spawn. Symétrique du contrat best-effort existant de apply_mcp_config (lifecycle.rs:2422-2425). ≠ resolve_opencode_provider_api_key (échec dur). Le seed est mou.
  3. Pas de régression local/custom : opencode_config_json (llamacpp) et opencode_provider_config_json(custom==Some) byte-identiques (rendu non touché ; seed additif orthogonal).
  4. Pas de régression built-ins : anthropic/openai/openrouter (custom==None, hardcodés) restent fonctionnels.
  5. Exclusion mutuelle opencode vs opencodeProvider (#97) : non touchée.
  6. Source de vérité unique du chemin models.dev : opencode_models_cache_path() réutilisée.
  7. Cohérence picker↔spawn : modèles résolvables au spawn = sur-ensemble de ceux du picker (même fichier source).

Hors périmètre (ne pas faire ici)

  • Résolution providers pour projets remote (SSH/WSL) (picker lit déjà le cache hôte — incohérence pré-existante). Fix corrige le cas local, ne régresse pas le remote.
  • Déduplication du rendu lifecycle↔infra (opencode_provider_config_json ×2) — on factorise uniquement le seeder.
  • Aucune modif UI/wizard.

QA — 2 couches

  1. Unitaire application (automatisable, déterministe) :
    • Partie pure seed_from_bytes(fs, dest, src_bytes) : bytes présents → assert <dest>/opencode/models.json écrit identique ; None → aucun fichier et pas d'erreur.
    • Non-régression rendu : JSON de opencode_config_json et opencode_provider_config_json(custom==Some/None) byte-identiques (tests existants lifecycle.rs:4428+ verts).
    • ⇒ Ne prouve pas la résolution réelle par OpenCode.
  2. Intégration / réelle-exécution (gated, propriété QA) : spawn réel d'OpenCode avec profil zai (custom==None) contre un cache models.dev fixture contenant zai ; assert pas de fallback glm-5.2 (modèle zai/<choix> effectivement utilisé). Gater #[ignore]/env (clé API + réseau). C'est ce test qui prouve le bug corrigé.

Risque résiduel + escalade

  • Inconnu empirique : est-ce qu'OpenCode au démarrage tente de rafraîchir models.dev (écrase notre copie, ou hang sans réseau) ? OPENCODE_DISABLE_AUTOUPDATE=1 ne vise que l'auto-update du binaire, pas sûr qu'il couvre le refresh models. QA doit vérifier sur le test d'intégration. Si le refresh défait B2 → escalader vers (A) : capturer npm+baseURL réels par provider dans le parseur models.dev et peupler custom.

Topologie

  • Branche : feature/ticket98-opencode-modelsdev-cache-seed depuis develop@69e5878 (#97 exclusion mutuelle prérequis y est fusionné : 0f0a76d + merge 5533073). crates/ propre.
  • Carnet #98 version 2 = source de vérité du cadrage.

Lié

  • #97 (relatesTo) : exclusion mutuelle provider — prérequis livré.