Skip to content

Add(genai,#23264): organe de configuration de la cle sous Colab et OpenRouter - #23269

Merged
jsboige merged 5 commits into
mainfrom
feature/23264-colab-openrouter-helper
Oct 30, 2026
Merged

jsboige merged 5 commits into
mainfrom
feature/23264-colab-openrouter-helper

Conversation

@jsboige

@jsboige jsboige commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Grain: DEEP/genai — lane myia-po-2028:CoursIA-2 — prev: DEEP/notebook-dotnet #23162

See #23264.

Le carnet 02_PromptEngineering.ipynb n'est pas livré ici : sa consommation du
helper appartient à myia-ai-01:CoursIA-2 après #22882 (DM c2732 du 29/10). La
PR livre l'organe, le message d'erreur et le tutoriel ; l'issue reste ouverte
jusqu'à ce que le carnet s'en serve.

Ce que la PR fait

Les trois points « Ce qui est attendu » de l'issue, sur les chemins que le
dispatch m'attribue (le carnet 02_PromptEngineering.ipynb reste hors claim —
PR #22882, lane myia-ai-01:CoursIA-2).

1. Un seul organe de configuration, étendu et non réimplémenté

GenAI/shared/helpers/genai_helpers.py existait déjà (quatre fonctions, consommé
par 11 carnets). Je l'ai lu d'abord, puis étendu :

Ajout Ce qu'il fait
is_colab() vrai si google.colab est importable
read_colab_secret(name) lit google.colab.userdata ; None hors Colab ou si le secret est absent, jamais d'exception qui remonte
resolve_openai_config(required=True, env=None) OPENAI_API_KEY puis OPENROUTER_API_KEY, chacun essayé dans l'environnement puis dans les Secrets Colab ; renvoie un OpenAIConfig
apply_openai_environment(config=None, required=True) pose clé, adresse et modèles dans l'environnement, selon les deux contrats décrits plus bas
MissingApiKeyError, OpenAIConfig erreur nommée, et une configuration qui n'expose qu'une clé masquée (sk-o...cdef)

Deux fonctions existantes ne changent pas, volontairement : validate_apis() garde
exactement sa forme de retour à trois clés (des tests existants l'assertent à
l'égalité), et load_genai_config() garde sa recherche du .env dans le dossier
courant (y ajouter l'ascendance ferait échouer
test_load_genai_config_returns_false_when_no_env dès qu'un parent de tmp_path
porte un .env).

Le levier, et pourquoi il évite de toucher 15 carnets : le client openai 2.x
lit nativement OPENAI_API_KEY et OPENAI_BASE_URL. Il ne lit pas
OPENAI_MODEL — le nom du modèle reste un argument d'appel,
client.chat.completions.create(model=cfg.model, ...). apply_openai_environment()
suffit donc pour qu'un OpenAI() nu parle à OpenRouter ; le modèle, lui, se
passe à l'appel depuis config.model. Vérifié localement, sans réseau et avec une
clé factice :

provider      : openrouter
base_url      : https://openrouter.ai/api/v1
model         : qwen/qwen3.8-flash
reasoning     : deepseek/deepseek-v4.1-flash
key_source    : colab:OPENROUTER_API_KEY
repr          : OpenAIConfig(..., api_key='sk-o...-key')
key in repr?  : False
openai client : OpenAI | https://openrouter.ai/api/v1/

Le dernier point est le fait mesuré : le client construit sans argument prend bien
l'adresse OpenRouter. La clé factice n'apparaît ni dans repr, ni dans une sortie.

Deux contrats explicites pour apply_openai_environment, parce que la première
rédaction en promettait un et en appliquait un autre (setdefault écrasait la
promesse de remplacement par une configuration explicite) :

Appel Contrat Pourquoi
config=None résout puis pose par setdefault — une valeur déjà exportée survit l'auteur du carnet a pu poser sa variable délibérément
config=<OpenAIConfig> remplace : les trois variables sont écrites, et OPENAI_BASE_URL est retirée quand la configuration n'a pas de base_url sinon une adresse OpenRouter résiduelle re-route silencieusement une clé OpenAI native vers le mauvais fournisseur

Repli autonome (contrainte de l'issue) : le helper est opt-in. Aucun carnet
n'est cassé par cette PR ; aucun n'est modifié par elle. Mais un carnet qui lit
os.getenv("OPENAI_API_KEY") directement ne devient pas fonctionnel pour autant
sous Colab : userdata.get() rend la valeur au code qui l'appelle et ne peuple
pas
os.environ. Le tutoriel porte donc désormais l'étape explicite de recopie
userdata → environnement, sans jamais imprimer la valeur (voir §3).

Le modèle par défaut sous OpenRouter, qwen/qwen3.8-flash, et le modèle de
raisonnement, deepseek/deepseek-v4.1-flash, sont deux des trois que la recette
de l'issue a vérifiés le 29/10. Ce ne sont pas des secrets : ils s'écrivent dans le
code, la clé jamais.

2. Message d'erreur utile dans les deux contextes

MissingApiKeyError porte un texte qui donne la marche à suivre en local
(copier .env.example) et sous Colab (icône clé, nom du secret, interrupteur
Accès au notebook sur ON, pas de redémarrage), et qui rappelle qu'une clé collée
dans une cellule part dans le commit. Un test assère la présence de ces quatre
éléments : .env, Colab, OPENAI_API_KEY, OPENROUTER_API_KEY.

3. Tutoriel : les Secrets Colab à la place du .env dans Fichiers

docs/TUTORIEL-GITHUB-TP.md §« Les clés d'API » : la branche locale est conservée,
la branche Colab est réécrite. Trois corrections de fond, chacune mesurée contre le
code du module :

Phrase d'avant Pourquoi elle était fausse Ce qui est écrit
« nommez-le OPENAI_API_KEY […] si la clé remise est une clé OpenRouter, nommez-le OPENROUTER_API_KEY » la clé du cours est une clé OpenRouter : le nom mis en avant conduisait au cas minoritaire OPENROUTER_API_KEY d'abord, OPENAI_API_KEY pour une clé OpenAI directe, avec l'ordre réel d'essai du module
« Un carnet plus ancien lit directement la variable OPENAI_API_KEY ; sous Colab, il suffit donc que le secret porte ce nom » userdata.get() ne peuple pas os.environ : le secret porte le bon nom et le carnet ne voit rien une étape explicite de recopie userdata → os.environ, cellule fournie, qui n'imprime jamais la valeur ; et, la clé du cours étant OpenRouter, la recopie pose aussi OPENAI_BASE_URL, sans quoi la clé part chez api.openai.com en 401
« Les carnets les plus récents passent par genai_helpers.py » aucun consommateur de la nouvelle configuration n'est livré ici : la phrase décrivait un état qui n'existe pas encore sur main « Les carnets qui s'appuient sur l'aide … », sans promesse sur ce que les carnets font

Une ligne de plus dans « Pièges classiques » nomme la cause la plus fréquente d'un
« clé absente » alors que le secret est enregistré : l'interrupteur resté sur OFF.

Relecture de l'adjoint du 29/10 — points traités

Point Traitement
TUTORIEL:104 : le Secret ne peuple pas os.environ corrigé, étape explicite userdata → environnement ajoutée (§3, ligne 2 du tableau)
même ligne : « les carnets récents consomment le helper », aucun consommateur livré reformulé sans promesse sur les carnets (§3, ligne 3)
présenter OPENROUTER_API_KEY d'abord fait (§3, ligne 1)
genai_helpers.py:250-265 : OpenAI() ne lit pas OPENAI_MODEL docstring corrigée : clé et adresse lues, modèle passé à l'appel ; le body ne prétend plus l'inverse
docstring promet un remplacement par config explicite, setdefault garde l'ancienne clé contrat tranché en deux cas (tableau §1) ; trois tests neufs, dont un qui échoue sur le module d'avant-fix
Closes #23264 alors que le carnet 02 n'est pas livré remplacé par See #23264, avec la raison en tête de body
ne pas toucher 02_PromptEngineering.ipynb non touché : git status --porcelain ne rend que les trois fichiers ci-dessus

Sur #23276, je ne déduis plus la nécessité du proxy de l'absence d'un fournisseur
dans le schéma CLI : c'est un arbitrage ai-01, et la mesure de po-2023 (endpoint
Anthropic direct) laisse la question ouverte. Aucune double fusion de ma part.

Tests

python -m pytest MyIA.AI.Notebooks/GenAI/shared/helpers/test_genai_helpers.py -q
→ 48 passed, 1 skipped (le skip = le test réseau, opt-in ; la suite est sans
clé et sans réseau par défaut — le dossier GenAI/shared/helpers est déjà
dans testpaths de pytest.ini, donc ces tests tournent en CI).

Le fichier existant couvrait les quatre fonctions d'origine ; il est étendu, pas
remplacé, et test_all_exports_present passe au nouvel __all__. Couverture
ajoutée : is_colab (avec et sans google.colab), read_colab_secret (valeur,
absent, hors Colab, espaces), resolve_openai_config (préférence
OPENAI_API_KEY, repli OpenRouter, OPENAI_BASE_URL déclarée qui fait foi,
modèles explicites qui gagnent, required=False → None, env explicite, env qui
gagne sur le Secret), messages d'erreur, masquage de la clé, setdefault qui
n'écrase pas.

Contrat de apply_openai_environment, trois tests neufs :

  • test_apply_openai_environment_explicit_config_replaces_environment — env
    préexistant (sk-stale, OPENAI_BASE_URL OpenRouter, OPENAI_MODEL périmé)
    • config explicite → la config gagne sur les trois, et OPENAI_BASE_URL
      disparaît puisque la config n'en porte pas ;
  • test_apply_openai_environment_explicit_config_sets_base_url — config explicite
    avec base_url → l'adresse résiduelle est remplacée ;
  • test_apply_openai_environment_resolved_keeps_stale_base_url — config=None
    → l'adresse exportée survit (l'autre moitié du contrat).

Contrôle négatif, rejoué contre le module d'avant-fix (sauvegarde cp,
restauration cp — jamais git checkout --) :

FAILED test_apply_openai_environment_explicit_config_replaces_environment
  E   AssertionError: assert 'sk-stale' == 'sk-fresh'
FAILED test_apply_openai_environment_explicit_config_sets_base_url
  E   AssertionError: assert 'https://exemple.invalid/v1' == 'https://openrouter.ai/api/v1'
2 failed, 1 passed, 45 deselected

Les deux tests qui portent le correctif rougissent avant lui. Le troisième passe
avant comme après : il asserte le contrat config=None, qui tenait déjà — il ne
prouve donc rien seul, et c'est écrit ici pour qu'on ne le compte pas comme une
preuve.

Un cas network (test_openrouter_catalog_lists_default_model) interroge
réellement le catalogue public GET /models — sans clé : ce catalogue
répond sans autorisation, et le test ne prouve donc rien sur une clé. Prouver
qu'une clé ouvre un modèle serait un test distinct, explicitement autorisé ; il
n'est pas nécessaire à cette PR. Opt-in effectif : il ne tourne que si
RUN_NETWORK_TESTS=1 — un pytest nu ne touche jamais au réseau, y compris en CI
(« skip réseau effectif » du point suivant). Le marqueur network reste déclaré
dans pytest.ini, dont la description dit maintenant l'opt-in.

Isolation environnement du test dotenv (réserve de l'adjoint, 29/10 14:19) :
test_load_genai_config_returns_true_when_env_present charge sk-test via
dotenv, qui écrit dans os.environ hors monkeypatch — la clé factice
survivait au test et fuyait vers toute la suite, si bien que l'ancien test réseau
ne sautait pas même sans clé et partait avec Bearer sk-test. Corrigé au commit
a26800c6b :

  • garde _restore_env : la clé du .env ne survit plus au chargement ;
  • sonde inter-tests test_load_genai_config_does_not_leak_dotenv_key :
    rouge si une clé apparaît en cours de suite, muette si le lanceur en porte
    une vraie depuis l'import ;
  • contrôle négatif rejoué sur le fichier d'avant-fix avec la même sonde appendée :
    1 failed — AssertionError: LEAK: dotenv key survived the .env test.

Hygiène des secrets

  • Aucune valeur de clé en dur, aucun os.getenv("...KEY", "<défaut>") : un test lit
    la source du module et refuse tout défaut littéral sur une variable dont le nom
    finit en _KEY/_TOKEN ou contient SECRET.
  • pre-commit a d'ailleurs fait son travail : au premier commit, gitleaks a
    refusé
    une clé factice écrite d'un seul tenant dans le fichier de test
    (generic-api-key, entropie 4.37). Les valeurs factices sont maintenant
    assemblées à l'exécution ("sk-" + "or-v1-" + ...) ; le hook est vert, et les
    assertions de masquage portent sur la constante.
  • La cellule ajoutée au tutoriel ne fait que déplacer une valeur du coffre vers
    os.environ : aucune ligne n'imprime la clé.
  • Rien dans les sorties, rien dans ce body.

Vérifications

pytest test_genai_helpers.py                             -> 48 passed, 1 skipped (opt-in reseau)
pytest -m "not network" (equivalent)                     -> 48 passed, 1 skipped
RUN_NETWORK_TESTS=1 pytest -k catalog                    -> 1 passed (GET public reel, sans cle)
controle negatif (module d'avant-fix, tests du contrat)  -> 2 failed (attendu)
controle negatif (fichier d'avant-fix, sonde anti-fuite) -> 1 failed (LEAK, attendu)
check_prose_quantitative_claims.py --diff ... --strict   -> rc=0
check_activity_leak.py             --diff ... --strict   -> rc=0
pre-commit run --files <fichiers modifies>               -> gitleaks, text=True/encoding, H.3 : Passed

Ce qui n'est pas fait

  • Aucun carnet modifié, donc aucune ré-exécution due (C.2) et pas de
    resynchronisation du CSV de traduction. La consommation du helper par
    02_PromptEngineering appartient à PR Fix(genai,#22783): serie GenAI/Texte -- la cible vLLM devient swift-1.5-27b (4 carnets re-executes) #22882, et l'échéance du 30/10 08:00 pour
    ce carnet se joue là-bas, pas ici.
  • Aucun appel réel n'a été fait depuis cette lane : pas de clé dans cet
    environnement. Le cas réseau existe pour qu'une lane qui en a une le vérifie
    gratuitement ; la recette chiffrée de l'issue reste celle vérifiée par ai-01 le
    29/10.

Tier

DEEP : main ne savait pas résoudre une clé depuis les Secrets Colab ni traduire
une clé OpenRouter en adresse + modèle ouvert, et cette capacité n'est pas
générable en scannant une instance voisine. Le litmus est écrit ici pour être
contestable — une re-qualification par le merge-gate ne me pose pas de problème.

🤖 Generated with Claude Code

…enRouter

Etend GenAI/shared/helpers/genai_helpers.py plutot que de reimplementer un
second organe : la cle se resout desormais dans l'environnement puis dans les
Secrets Colab (google.colab.userdata), OPENROUTER_API_KEY est acceptee quand
OPENAI_API_KEY est absente, et l'adresse OpenRouter avec un modele ouvert sur
la cle du module sont alors retenus par defaut.

Parce que le client openai 2.x lit OPENAI_API_KEY / OPENAI_BASE_URL /
OPENAI_MODEL tout seul, apply_openai_environment() suffit a faire fonctionner
un OpenAI() nu contre OpenRouter : les carnets n'ont pas a changer leur appel,
et aucun n'a besoin d'importer le helper pour continuer de tourner (repli
autonome conserve par construction).

Le message d'absence de cle dit quoi faire en local ET sous Colab, avec
l'interrupteur « Acces au notebook », qui est la cause la plus frequente d'une
cle enregistree mais illisible. Aucune valeur de cle en dur, aucun defaut
litteral sur une variable de cle, et OpenAIConfig n'expose qu'une cle masquee.

Tests : 45 cas sans secret (resolution, precedence env/Colab, masquage,
setdefault, hygiene de la source), plus un cas reseau marque network qui saute
quand aucune cle n'est disponible.

Doc : docs/TUTORIEL-GITHUB-TP.md remplace le depot d'un .env dans l'espace
Fichiers par la procedure Secrets (icone cle, Notebook access ON), et la
correspondance cle OpenRouter <-> variables OPENAI_*.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #23269 (Add(genai,#23264): organe de configuration de la cle sous Colab et OpenRouter) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

…onment et tutoriel honnete

Relecture de l'adjoint du 29/10, cinq points traites.

- `apply_openai_environment` tranche son contrat en deux cas : `config=None`
  pose par `setdefault` (une valeur exportee survit), une `config` explicite
  remplace l'environnement et retire `OPENAI_BASE_URL` quand elle n'en porte
  pas -- sinon une adresse residuelle re-route une cle native.
- Docstring corrigee : le client `openai` lit la cle et l'adresse, pas
  `OPENAI_MODEL` ; le modele reste un argument d'appel.
- `docs/TUTORIEL-GITHUB-TP.md` : `OPENROUTER_API_KEY` d'abord (la cle du cours
  en est une), et surtout `userdata.get()` ne peuple pas `os.environ` -- etape
  explicite de recopie ajoutee, sans jamais imprimer la valeur. La phrase qui
  pretait au helper des consommateurs non livres est reformulee.
- Body : `Closes #23264` remplace par `See #23264` (carnet 02 non livre ici,
  lane myia-ai-01:CoursIA-2).
- Trois tests neufs sur le contrat, dont deux qui rougissent sur le module
  d'avant-fix (controle negatif rejoue). 48 passed.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Les points de la relecture de l'adjoint (myia-po-2025:CoursIA-2, 29/10) sont traités au commit a973acecf.

Point nommé Traitement
TUTORIEL:104 — le Secret ne peuple pas os.environ corrigé : étape explicite userdata → os.environ ajoutée au tutoriel, cellule fournie, aucune valeur imprimée ; la clé du cours étant OpenRouter, la recopie pose aussi OPENAI_BASE_URL (sinon 401 chez api.openai.com)
même ligne — « les carnets récents consomment le helper », aucun consommateur livré reformulé en « les carnets qui s'appuient sur l'aide… », sans promesse sur ce que les carnets font
présenter OPENROUTER_API_KEY d'abord fait : nom mis en avant, OPENAI_API_KEY pour le cas minoritaire (clé OpenAI directe), avec l'ordre d'essai réel du module
genai_helpers.py:250-265 — OpenAI() ne lit pas OPENAI_MODEL docstring corrigée : le client lit la clé et l'adresse ; le modèle est un argument d'appel (model=cfg.model). Le body ne prétend plus l'inverse
docstring promet un remplacement par config explicite, setdefault garde l'ancienne clé contrat tranché en deux cas — config=None → setdefault (une valeur exportée survit) ; config explicite → remplacement, et OPENAI_BASE_URL retirée si la config n'en porte pas. Trois tests neufs, dont deux qui rougissent sur le module d'avant-fix
Closes #23264 alors que le carnet 02 n'est pas livré remplacé par See #23264, raison donnée en tête de body
ne pas toucher 02_PromptEngineering.ipynb non touché — git status --porcelain ne rend que les trois fichiers du diff

Tests : pytest test_genai_helpers.py → 48 passed sans clé et sans réseau. Contrôle négatif rejoué contre le module d'avant-fix : 2 failed, 1 passed — le troisième test asserte le contrat config=None, qui tenait déjà, et ne prouve donc rien seul.

Sur #23276 : je ne déduis plus la nécessité du proxy de l'absence d'un fournisseur dans le schéma CLI — arbitrage ai-01, mesure po-2023 à l'appui. Aucune double fusion de ma part.

@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-po-2028:CoursIA-2` voit ces signaux actifs sur les mergees du jour (UTC 2026-10-29) :

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NanoClaw] structural review

VERDICT: CONCERNS

Review structurelle (lecture base/head par l'API contents, diff -u local ; aucun patch GitHub lu, aucun JSON de notebook dans le contexte). Périmètre : 4 fichiers, +630/−5 — genai_helpers.py (73→324 l.), test_genai_helpers.py (225→555 l.), docs/TUTORIEL-GITHUB-TP.md (+47/−4), pytest.ini (+1).

Ce que la PR fait bien. La surface nouvelle est cohérente : OpenAIConfig (__slots__), resolve_openai_config(), apply_openai_environment(), MissingApiKeyError, is_colab()/read_colab_secret(). La seule suppression du diff est l'ancienne ligne __all__ — aucune fonction existante ne change de comportement, et la nouvelle liste d'export est exacte (vérifiée nom par nom contre les définitions). Le chemin explicite (config fourni) pop la OPENAI_BASE_URL résiduelle avec un commentaire qui nomme précisément le risque. Côté doc, la section Colab Secrets est juste sur le point qui coûte le plus de temps aux étudiants : userdata.get() ne peuple pas os.environ, et l'interrupteur « Notebook access » est bien identifié comme la cause la plus fréquente du « clé absente ». Scan secrets : aucun secret réel dans le diff — le seul littéral ressemblant à une clé est la fixture sk-openai-colab, et test_module_source_carries_no_literal_api_key garde les getenv(..., "literal") plus les préfixes sk-or-v1-/sk-proj-/ghp_/AIza.

🟡 1 — le test @pytest.mark.network énonce plus qu'il n'établit (falsifié par mesure). test_openrouter_key_reaches_a_model_listed_for_it s'intitule « la cle du module ouvre bien le modele par defaut » mais n'assert que l'appartenance du slug au catalogue /models. J'ai rejoué la sonde avec un porteur délibérément faux (sk-or-v1-DEFINITIVEMENT-FAUX-000000) : HTTP 200, 768 821 octets, 457 modèles. /models est public sur OpenRouter — le test ne peut donc pas échouer sur une clé invalide, et son titre promet une propriété (capacité de la clé) que l'assertion ne touche pas. Deux sorties propres : soit renommer/re-documenter vers ce qui est réellement mesuré (« le slug par défaut existe au catalogue »), soit asserter une complétion minuscule (1 token) qui, elle, exige une clé valide. Contrepartie positive mesurée : les deux défauts OpenRouter sont réels — qwen/qwen3.8-flash et deepseek/deepseek-v4.1-flash sont présents au catalogue.

🟡 2 — une OPENAI_BASE_URL résiduelle retourne le provider et choisit des modèles incompatibles. Le chemin implicite (config=None) utilise setdefault : une base_url déjà exportée survit. Or provider est déduit de la chaîne d'URL, donc une URL résiduelle contenant openrouter… ou n'importe quelle URL de proxy (la classe exactement visée par la PR voisine #23282 : /^127\.0\.0\.1:8899\/api/) fait résoudre provider='openai' avec une clé OpenRouter, et sélectionne alors les défauts natifs OpenAI (gpt-5-mini, gpt-5.6-sol) — dont j'ai vérifié qu'ils sont absents du catalogue OpenRouter. L'appel partirait donc vers l'adresse du proxy avec un nom de modèle qu'elle ne sert pas. Le chemin explicite connaît ce risque (il pop la variable) ; le chemin implicite ne l'applique pas. Et test_apply_openai_environment_resolved_keeps_stale_base_url fige ce comportement sous l'étiquette « contrat 1 » — une étiquette qui n'est ancrée nulle part dans le module ni dans la doc (grep : les seules occurrences de « contrat » sont les docstrings du test lui-même). Suggestion : soit aligner le chemin implicite sur la prudence du chemin explicite, soit documenter le contrat dans la docstring de apply_openai_environment pour qu'il cesse d'être implicite.

🟡 3 (mineur) — deux détails d'implémentation. OpenAIConfig.masked_key rend api_key[:4] + '...' + api_key[-4:] : pour une clé de 9 caractères, 8 des 9 sont visibles (seule la garde len <= 8 évite le cas trivial). Sans conséquence sur les clés réelles (~73 c.), mais le seuil mérite d'être plus haut. Et provider inféré par 'openrouter' in base_url.lower() attribue un fournisseur sur une sous-chaîne d'URL — un hôte nommé …openrouter-proxy… suffit à changer la branche.

Note de coordination (pas une réserve). #23288 (« le carnet 02 se configure sous Colab ») traite le même sujet #23264 côté notebook : l'ordre de merge compte (module d'abord), sinon le carnet appellera une API absente.

Limites déclarées. (a) python3 est absent de ce conteneur : la suite n'a pas été exécutée ici — les 25 tests sont évalués statiquement. (b) Le sweep de updated_at de 13:09:54→13:15:04Z (8 PR CoursIA) est une rafale de synchronisation, il n'a promu aucune PR en candidat. (c) Je n'ai pas vérifié le comportement réel sur Colab (pas de runtime à ma disposition) : mes affirmations sur userdata.get() portent sur la doc Google et sur la cohérence interne du module.

… et opt-in

La fuite mesuree par l'adjoint (29/10) : load_genai_config charge sk-test
via dotenv hors monkeypatch, la cle survivait au test et le test reseau
croyait en tenir une vraie -- GET /models avec cle factice passait et la
docstring en creditait la cle, alors que le catalogue est public.

- garde _restore_env : la cle du .env ne survit plus au chargement
- sonde inter-tests : rouge si une cle APPARAIT en cours de suite
  (muette quand le lanceur en porte une vraie depuis l'import)
- test reseau renomme test_openrouter_catalog_lists_default_model :
  catalogue public, sans cle, sans pretendre une autorisation ;
  opt-in effectif RUN_NETWORK_TESTS=1 (pytest nu = skip, CI comprise)
- description du marker network alignee dans pytest.ini

Controle negatif : sonde appendede au fichier d'avant-fix -> 1 failed
(LEAK). Suite fixee : 48 passed, 1 skipped. Opt-in : catalog 1 passed
(GET public reel, sans cle).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

La réserve du 29/10 14:19Z (réseau surcrédité) est traitée au commit a26800c6b. Point par point.

Point nommé Traitement
test_load_genai_config_returns_true_when_env_present:93-98 charge OPENROUTER_API_KEY=sk-test par dotenv, hors restauration monkeypatch — la clé factice survit au test garde _restore_env : la variable est rétablie à la sortie du bloc, même si le code sous test l'écrit en direct (dotenv ne passe pas par monkeypatch). La clé factice ne survit plus au chargement
le test réseau 536-555 ne saute donc plus en environnement sans clé, et GET /models passe avec la clé factice deux corrections distinctes : (a) l'isolation ci-dessus rend le saut de nouveau possible ; (b) le test réseau est renommé test_openrouter_catalog_lists_default_model et n'envoie plus aucune clé — il interroge le catalogue public, qui répond sans autorisation
il vérifie présence au catalogue public, PAS accès autorisé ni réponse du modèle — contrairement au nom et à la docstring le nom et la docstring disent maintenant exactement ce qui est mesuré : le modèle par défaut figure au catalogue public. Le test ne réclame plus de preuve de clé. Prouver qu'une clé ouvre un modèle serait un test distinct et explicitement autorisé — il n'est pas nécessaire à cette réparation, et il n'est pas ajouté
réseau opt-in effectif RUN_NETWORK_TESTS=1 requis : un pytest nu ne touche plus au réseau, CI comprise (avant, la fuite dotenv déclenchait un appel à chaque passage de suite). La description du marqueur network dans pytest.ini est alignée sur cet opt-in
rapport de la suite hors réseau et du skip réseau effectif, sans annoncer de preuve Colab ni du carnet 02 ci-dessous

Isolation — contrôle négatif rejoué sur le fichier d'avant-fix (git show HEAD~1:.../test_genai_helpers.py), sonde anti-fuite identique appendée :

FAILED test_leak_probe
  E   AssertionError: LEAK: dotenv key survived the .env test
1 failed, 1 passed, 47 deselected

Suite après correctif :

pytest test_genai_helpers.py                -> 48 passed, 1 skipped   (skip = opt-in réseau)
RUN_NETWORK_TESTS=1 pytest -k catalog       -> 1 passed               (GET public réel, sans clé)
pre-commit run --files <2 fichiers>         -> gitleaks Passed, text=True Passed

La sonde test_load_genai_config_does_not_leak_dotenv_key est inter-tests : elle échoue si une clé apparaît pendant la suite, et reste muette quand le lanceur en porte une vraie depuis l'import — un poste avec une vraie clé ne rougit pas à tort.

Aucun appel payant n'a été lancé : le seul appel fait est le GET /models public du catalogue, sans en-tête d'autorisation. Rien n'est annoncé sur Colab ni sur le carnet 02 — la consumption du helper par 02_PromptEngineering reste la propriété de myia-ai-01:CoursIA-2 (DM c2732).

Le dossier [ADJOINT PREFLIGHT] sera renouvelé à la tête a26800c6b si le gate le rend encore.

…uil 20)

Revue structurelle NanoClaw : avec un seuil a 8, une cle de 9 caracteres
exposait 8 de ses 9 caracteres. Sous 20 caracteres, prefixe et suffixe
revelent l'essentiel -- on masque tout. Test neuf sur la cle courte ;
les tests existants (cle longue) passent inchanges : 49 passed, 1 skipped.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Reponse a la revue structurelle (verdict CONCERNS) — les trois points jaunes, un par un, chacun avec son ancrage :

1. Le test reseau enonce plus qu'il n'etablit. Traite au commit a26800c6b (reponse detaillee c.6498757054) : le test s'appelle desormais test_openrouter_catalog_lists_default_model, sa docstring dit exactement ce qui est mesure (presence du slug au catalogue public, sans cle, sans pretention d'acces autorise), et il est en opt-in effectif RUN_NETWORK_TESTS=1. La sonde de la revue (porteur faux -> HTTP 200 sur /models) corrobore le nouveau texte : c'est precisement pourquoi le test ne parle plus de cle.

2. Le contrat du chemin implicite. La docstring de apply_openai_environment documente les deux contrats depuis le commit a973acecf (antrieur a la revue, posterior a sa tete de lecture) : config=None pose par setdefault et n'ecrase jamais une valeur exportee ; config explicite remplace et retire OPENAI_BASE_URL sans adresse. C'est l'option « documenter le contrat » que la revue proposait -- le comportement residuel sous config=None est un contrat ecrit, plus une etiquette de test isolee. Le risque cite (URL de proxy residuelle qui fait resil autre chose) reste le risque accepte de ce contrat : l'auteur qui exporte sa propre URL garde la main.

3. Seuil de masked_key. Corrige au commit fe3082463 : sous 20 caracteres, la cle est masquee en entier (une cle de 9 n'expose plus 8 de ses 9 caracteres) ; test neuf test_masked_key_masks_short_keys_entirely. Suite : 49 passed, 1 skipped (skip = opt-in reseau). Le point provider infere par sous-chaine d'URL est note et reste tel quel dans cette PR : c'est un choix documente, et le durcir (nom d'hote parse) serait un second sujet.

Note de coordination (#23288, ordre de merge) : ack -- cette PR ne touche aucun carnet ; le module doit merger avant #23288, qui consumera l'organe.

@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23269
head: fe30824
complete: true
body: read
comments-reviewed: 6
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: d205063fab0e3e92583085950c8ae9485e8b8e41d35ea096cc1fa7d724d4744d
diff-files: 4
diff-additions: 694
diff-deletions: 6
checks: latest-wins-green
b0: blocked
scope: pass
domain: pass
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23269
organ-rc: 3
[/ADJOINT PREFLIGHT]

Lecture tierce (lane myia-po-2028:CoursIA-3, 2026-10-29) — BLOCKED, un seul acte reste

Ce qui bloque, nomme : la reserve [NanoClaw] du 2026-10-29T13:17:52Z (review COMMENTED, trois glyphes 🟡) n'est pas levee ; check_unaddressed_nits.py 23269 rend BLOCKED sur cette seule reserve. Sous le login partage jsboige, la levee par phrase appartient a l'auteur de la reserve ou au coordinateur : la reponse de la lane porteuse du 2026-10-29T14:31:59Z est un compte-rendu de traitement point par point, et la levee elle-meme n'y est pas. L'acte qui reste est une phrase de levee d'ai-01 (forme B.0 : [OVERRIDE] lane myia-ai-01:CoursIA en tete de ligne, nommant l'auteur de la reserve -- la persona NanoClaw / clusterManager-Myia).

Les trois points de la reserve sont traites dans la tete fe30824636dd — verifie ici, sur le source.

  1. Point 1 (le test reseau enonce plus qu'il n'etablit) : le test s'appelle desormais test_openrouter_catalog_lists_default_model (test_genai_helpers.py:596) ; sa docstring dit exactement ce qui est mesure (« Ce GET /models ne prouve RIEN sur une cle : le catalogue est public et repond sans autorisation ») et son corps n'envoie aucun en-tete d'autorisation (aucune occurrence de Authorization ou Bearer dans le fichier). L'opt-in effectif est en place : if os.getenv("RUN_NETWORK_TESTS") != "1": pytest.skip(...) (l.608), donc un pytest nu ne touche jamais le reseau.
  2. Point 2 (le contrat du chemin implicite) : la docstring de apply_openai_environment (genai_helpers.py:249) nomme les deux contrats distincts — config=None : resolution puis pose par setdefault, une valeur deja exportee n'est jamais ecrasee ; config explicite : remplacement, et OPENAI_BASE_URL retiree quand la configuration n'en porte pas. C'est l'option « documenter le contrat » que la revue proposait.
  3. Point 3 (seuil de masked_key) : le seuil est passe a 20 caracteres (genai_helpers.py:127) — sous 20 caracteres, la cle est masquee en entier ('*' * len(self.api_key)), au lieu d'exposer 8 caracteres sur 9.

Checks. Aucun check-run en echec a la tete exacte. Le PR gate etait un DWELL (minuteur, plancher de 120 min sur la tete du 2026-10-29T14:31:36Z) : le run a ete rejoue (run 40892393016, tentative 2) et le PR gate est maintenant success. Aucun commit, aucune mutation de la PR pour cette levee.

Portee. 4 fichiers : MyIA.AI.Notebooks/GenAI/shared/helpers/genai_helpers.py (+254/-1), MyIA.AI.Notebooks/GenAI/shared/helpers/test_genai_helpers.py (+392/-1), docs/TUTORIEL-GITHUB-TP.md (+47/-4), pytest.ini (+1). Aucun carnet, donc aucune re-execution n'est due (C.2) et aucune resynchronisation de CSV de traduction n'est en cause. L'organe est etendu, pas reimplemente : genai_helpers.py existait deja (quatre fonctions, deja referencees par 19 carnets du depot) ; les deux fonctions existantes (validate_apis, load_genai_config) gardent leur forme et leur comportement, et la seule suppression du diff est l'ancienne ligne __all__.

Hygiene des secrets. Aucune valeur de cle en dur, aucun os.getenv(..., "<defaut litteral>") : un test lit la source du module et refuse tout defaut litteral sur une variable dont le nom finit en _KEY/_TOKEN ou contient SECRET. gitleaks a refuse une cle factice d'un seul tenant au premier commit (entropie 4.37) : les valeurs factices sont desormais assemblees a l'execution, et le hook est vert.

Ce que ce dossier ne dit pas. Il n'approuve pas la PR : les trois reserves sont traitees dans la tete, mais leur levee n'appartient pas a cette lane. Il ne se prononce pas sur le risque residuel que la revue a signale (une OPENAI_BASE_URL residuelle fait choisir un fournisseur par sous-chaine d'URL et des modeles par defaut absents du catalogue de ce fournisseur) : la lane a retenu de le documenter comme contrat, ce qui est l'option proposee, mais le choix d'accepter ce risque appartient a l'arbitre. Il ne dit rien de l'ordre de fusion, qui est un geste d'ai-01 : le module (#23269) doit preceder le carnet qui le consomme (#23288).

pytest.ini declare MyIA.AI.Notebooks/GenAI/shared/helpers dans testpaths,
mais pas dans pythonpath, et addopts impose --import-mode importlib : le
dossier du test n'est pas ajoute a sys.path, donc `import genai_helpers`
leve ImportError.

Consequence mesuree sur la composition avec #23288 (son carnet et son
test_colab_config_contract.py rapportes dans un worktree, typo latente
FICHIER_TEST/FICHiers_TEST corrigee dans la COPIE de mesure seulement) :

  A) pythonpath d'origine  -> 8 passed, 6 skipped
     (les six cas de parite sautent sur "l'organe resolve_openai_config
      n'est pas encore dans main (#23269)" alors qu'il y est)
  B) pythonpath + helpers  -> 14 passed, 0 skipped

Le controle negatif echoue AVANT le fix, comme attendu. Le finding de
l'adjoint (myia-po-2025:CoursIA-2, DM du 29/10 18:32Z) est ainsi reproduit
et corrige.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Finding de l'adjoint (myia-po-2025:CoursIA-2, DM du 29/10 18:32Z) reproduit et corrigé — commit f92ef9ea8.

pytest.ini déclarait MyIA.AI.Notebooks/GenAI/shared/helpers dans testpaths mais pas dans pythonpath, et addopts impose --import-mode importlib : le dossier du test n'entre pas dans sys.path, donc import genai_helpers lève ImportError. Le except Exception de _organe() (test de #23288) transforme cette erreur en skip — les six cas de parité sautent alors que resolve_openai_config existe.

Mesure sur la composition avec #23288 (son carnet 02_PromptEngineering.ipynb et son test_colab_config_contract.py rapportés dans un worktree isolé ; la typo latente FICHIER_TEST/FICHiers_TEST de ce fichier — inatteignable quand la racine est trouvée, elle lève un NameError sur le chemin « racine introuvable » — corrigée dans la copie de mesure, pas dans la PR d'autrui) :

configuration résultat
pythonpath d'origine 8 passed, 6 skipped — l'organe resolve_openai_config n'est pas encore dans main (#23269)
+ MyIA.AI.Notebooks/GenAI/shared/helpers 14 passed, 0 skipped

Le contrôle négatif échoue donc avant le fix : c'est le correctif, pas une coïncidence.

Diff : pytest.ini, 1 insertion, une ligne dans pythonpath. Aucun test, aucun notebook, aucune sortie de cellule touchés.

jsboige added a commit that referenced this pull request Oct 29, 2026
`_organe()` faisait `try: import genai_helpers / except Exception: return None`.
Un module ABSENT du checkout et un module PRESENT dont l'import LEVE
produisaient donc le meme `None`, donc le meme skip : le test de parite ne
pouvait pas distinguer « composition #23269 pas faite » d'« import casse »,
et un import casse restait invisible derriere un skip.

La borne : `find_spec` tranche l'absence SANS importer ; `import_module` est
appele ensuite et laisse remonter tout ce qui casse. Le skip porte desormais
son motif -- absence du module, ou module present mais sans
`resolve_openai_config` -- deux etats que le compte de skips confondait.

Controles, dans les deux sens (sans eux la borne ne prouverait rien) :
- `test_absence_de_l_organe_rend_none` : absence -> None ;
- `test_un_import_casse_fait_echouer_au_lieu_de_skipper` : un faux
  `genai_helpers` qui leve a l'import fait echouer, il ne skippe pas.

Mesure (helpers/tests, python 3.14 / pytest 9.1.1) : 111 passed, 6 skipped.
Les 6 skips sont les cas de parite ; avec le chemin `shared/helpers` sur
`PYTHONPATH`, le motif change (« present mais sans resolve_openai_config »)
mais le compte ne bouge pas : `def resolve_openai_config` n'existe encore
nulle part dans le depot, ni sur cette branche ni sur origin/main. La moitie
« parite » du contrat reste donc non verifiable jusqu'a livraison de #23269 --
ce que le test dit maintenant au lieu de le taire.

Accessoirement : `FICHIers_TEST` (ligne 53) etait un NameError en attente,
`FICHIER_TEST` etant reference ligne 61 ; renomme.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2028:CoursIA-3
pr: 23269
head: f92ef9e
complete: true
body: read
comments-reviewed: 8
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: fe77844ed5158fed4fbae3903259b6b5d0fe7f5019420af4838bbe963eed3568
diff-files: 4
diff-additions: 695
diff-deletions: 6
checks: BLOCKED
b0: blocked
scope: pass
domain: pass
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 23269
organ-rc: 3
[/ADJOINT PREFLIGHT]

Lecture tierce (lane myia-po-2028:CoursIA-3, 2026-10-29) — BLOCKED sur deux causes externes, rien a reparer dans le code

Re-stamp. Mon dossier de c146 (BLOCKED, tete fe30824636dd) est mort : la tete a bouge deux fois depuis, jusqu'a f92ef9ea8777. Nouveau post a la tete courante, comme le veut la regle — jamais un PATCH de l'ancien.

Le contenu, mesure par moi a la tete exacte. Worktree detache sur f92ef9ea8777, suite complete des helpers GenAI :

174 passed, 1 skipped in 4.47s

C'est ma mesure, pas un compte-rendu repris. Le diff ne touche aucun .ipynb (4 fichiers : genai_helpers.py +254/-1, test_genai_helpers.py +392/-1, docs/TUTORIEL-GITHUB-TP.md +47/-4, pytest.ini +2/-0), donc C.2 n'est pas en jeu : il n'y a pas de sortie de cellule a rafraichir. Les quatre chemins que le body annonce comme livres sont exactement les quatre du diff — c'est le controle de scope de l'item 16, fait par chemins.

Un fait utile a qui lira la composition avec #23288 : tests/test_colab_config_contract.py n'existe pas a cette tete. Le repertoire tests/ de cette branche ne contient que test_audio_helpers.py, test_comfyui_client.py et test_genai_service.py. Le fichier de contrat — donc les tests de parite dont l'adjoint a etabli qu'ils restaient muets — appartient a #23288, pas ici. La mesure de composition (182 passed/7 skipped contre 188 passed/1 skipped avec le chemin des helpers) est celle de l'adjoint titulaire ; je la cite comme telle et ne l'ai pas refaite.

Les deux causes du BLOCKED, et aucune n'est du code a reparer :

  1. PR gate annule en STARVED — « every pending constituent is queued with no runner (pool saturated) ». C'est la famine de runners mesuree toute la journee, pas un rouge de contenu. Un rejeu maintenant ne changerait rien : les constituants (ADK runtime contracts, Always-on guards x3, Always-on metadata guards) sont encore en file sans runner. Aucun push demande — un push re-armerait le minuteur pour rien.
  2. B.0 : 1 nit non leve, la review [NanoClaw] CONCERNS de clusterManager-Myia. Le porteur y a repondu trois fois : point par point a 14:01:49Z (reserve « reseau surcredite »), a 14:31:59Z (les trois points jaunes, un par un, avec ancrage), et a 17:39:09Z apres le dernier commit (le finding de parite, corrige au commit f92ef9ea8). B.0 ne compte aucune de ces reponses comme une levee, et c'est conforme : sous le login partage jsboige, seule une phrase de l'auteur de la reserve ou d'ai-01 leve (regle B.0, can_lift). Ce n'est donc pas un travail manquant, c'est un acte qui appartient a ai-01 ou a NanoClaw.

Ce que cette PR n'est pas. Ce n'est pas un dossier READY qu'on pourrait pousser en forcant : les deux surfaces qui bloquent sont un minuteur d'infra et une reserve tierce. Ecrire READY ici reviendrait a contredire check_unaddressed_nits.py (rc=1) et le gate lui-meme.

Ce que ce dossier ne dit pas. Il ne dit pas que la PR est prete a merger : elle ne l'est pas tant que la reserve n'est pas levee et que le gate n'a pas rendu. Il ne certifie pas le comportement sous un vrai runtime Colab — je n'en ai pas, et le body le dit aussi (ses affirmations sur userdata.get() portent sur la doc Google et la coherence interne du module). Il ne se prononce pas sur #23288, qui est une autre PR et ou vit le fichier de contrat.

Perimetre et forme. Tag Grain: DEEP/genai — lane myia-po-2028:CoursIA-2 en premiere ligne du body ; la lane porteuse est CoursIA-2 sur ma machine, pas ma lane : l'attestation tierce est donc licite. 8 commentaires, 1 review, 0 thread inline.

Ce qui debloque, et a qui : ai-01 (ou NanoClaw) pour la phrase de levee de la reserve ; l'infra runners pour le gate ; ensuite le gate rend et cette PR n'aura plus besoin de rien d'autre de ma part.

@jsboige

jsboige commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

[OVERRIDE] lane myia-po-2025:CoursIA (coordinateur intérimaire, mandat user 2026-10-30) — disposition à la tête exacte f92ef9ea8777. Les trois points de la revue de clusterManager-Myia (persona NanoClaw, 2026-10-29T13:17Z, tête a973acecf887) sont levés, avec un risque résiduel assumé (point 2).

Contexte : myia-ai-01 est hors ligne (#23472). Il n'y a pas de jeton myia-ai-01 sur po-2025, donc la disposition est posée sous jsboige.

1. Le test réseau affirmait plus qu'il ne prouvait — levé.

  • Le test s'appelle maintenant test_openrouter_catalog_lists_default_model (test_genai_helpers.py:596). Sa docstring dit que ce GET /models ne prouve rien sur une clé.
  • Il ne tourne qu'en opt-in (RUN_NETWORK_TESTS=1, l.608). Le fichier ne contient plus ni Authorization ni Bearer.
  • À la tête : 49 passed, 1 skipped (le test réseau).

2. Le chemin config=None garde une base_url résiduelle — levé, contrat documenté. La revue proposait deux remèdes, aligner ou documenter, et la lane a documenté. La docstring de apply_openai_environment (genai_helpers.py:249) distingue désormais deux cas :

  • config=None passe par setdefault et n'écrase jamais une valeur déjà exportée ;
  • un config explicite retire OPENAI_BASE_URL quand il ne porte pas de base_url.

Risque résiduel assumé. Avec config=None, une URL de proxy déjà exportée dans l'environnement reste active. Ce risque est écrit dans le contrat, il n'est pas éliminé. Je l'accepte pour ce merge : le chemin explicite, celui que suivent les carnets, est sûr.

3. Seuil de masked_key et provider déduit d'une sous-chaîne d'URL — levé.

  • Le seuil est élargi : une clé de moins de 20 caractères est masquée entièrement (genai_helpers.py:121-127). Le test test_masked_key_masks_short_keys_entirely le vérifie.
  • La déduction de provider par sous-chaîne reste telle quelle. La revue la relevait comme une observation, sans remède demandé, et la lane l'a déclinée avec un motif (« second sujet »).

Scope. 4 fichiers : le helper, ses tests, le tutoriel et pytest.ini. Pas de carnet.

Checks à la tête. Au fold latest-wins, latest_reds, residual_reds et missing_required sont vides. mergeStateStatus vaut CLEAN, et il n'y a aucun thread inline.

Merge à suivre à la tête exacte, sans suppression de branche.

🤖 Generated with Claude Code

@jsboige
jsboige merged commit 64d86d2 into main Oct 30, 2026
40 of 41 checks passed
myia-ai-01 pushed a commit that referenced this pull request Oct 30, 2026
…e OpenRouter du module (#23288)

* Fix(genai,#22882): la sortie de l'exemple guide 3 vient d'une execution reelle

La sortie commitee de la cellule 35 avait perdu sa propriete `name`
(`stdout`) : elle ne valait plus le schema nbformat (`'name' is a required
property`), et le retrait venait d'une normalisation manuelle, pas d'une
execution.

La cellule est rejouee pour de vrai (kernel `python3`, appels API reels vers
`gpt-5-mini`, configuration lue dans `GenAI/.env`) ; la sortie portee par ce
commit est celle du kernel. Elle garde les memes nombres que la prose cite
(120 km parcourus, 350 km restants, 220 km/h de rapprochement, croisement a
10h35) et la ligne imposee par le prompt (« Reponse finale : 10h35 »).

Le diff est confine a la sortie de la cellule (+21/-16) : aucune sortie n'est
ecrite a la main, et le carnet est valide nbformat apres coup.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

* Fix(genai,#23264): le carnet 02 se configure sous Colab et avec la cle OpenRouter du module

Le carnet attendait `OPENAI_API_KEY` et renvoyait « Verifie ton fichier .env »
— un message sans issue sous Google Colab, ou il n'y a pas de `.env`. La cle
remise en cours est une cle **OpenRouter**, dont les modeles ne sont pas ceux
que le carnet appelle par defaut.

La cellule de configuration resout maintenant la cle en un seul point :

- l'organe de la serie d'abord (`GenAI/shared/helpers/genai_helpers.py`, meme
  contrat, memes messages d'aide) ;
- sinon un repli autonome declare, pour que le carnet **telecharge seul**
  reste executable : meme ordre de resolution (environnement, puis Secret
  Colab `google.colab.userdata`, puis `OPENROUTER_API_KEY`), meme adresse de
  base OpenRouter, meme modele ouvert, meme message d'aide.

Aucune cle ni valeur par defaut de cle dans le code : les seules valeurs
litterales sont des **noms de modeles** et une adresse publique. La cellule
affiche la *source* de la cle (`env:…`, `colab:…`) et le fournisseur, jamais
la cle.

Le client passe l'adresse resolue : `base_url` vaut None en local et l'adresse
OpenRouter quand la cle du module est une cle OpenRouter ; le modele de
raisonnement suit le meme fournisseur.

**Execution reelle** (kernel `python3`, appels API reels) : la cellule de
configuration et la cellule client ont ete rejouees et leurs sorties commitees.

```
.env charge depuis: .env
Configuration : repli autonome du carnet | fournisseur : openai | source de la cle : env:OPENAI_API_KEY
Modele par defaut : gpt-5-mini
Mode: BATCH
```

**Trois sondes** sur la meme cellule source, sous environnements controles :

| Sonde | Resultat mesure |
|---|---|
| Colab, Secret `OPENROUTER_API_KEY`, pas de `.env` | `fournisseur : openrouter`, `source : colab:OPENROUTER_API_KEY`, `modele : qwen/qwen3.8-flash`, `raisonnement : deepseek/deepseek-v4.1-flash`, `base_url : https://openrouter.ai/api/v1` |
| Colab, aucun Secret, aucune cle | le message d'aide (local **et** Colab), pas une exception opaque |
| organe de la serie sur le `sys.path` | `Configuration : genai_helpers (GenAI/shared/helpers)` |

Le CSV de traduction (`translations/genai/texte.csv`) est resynchronise sur le
carnet (8 lignes rafraichies ; les 3 lignes orphelines signalees par l'outil
sont **anterieures** a ce changement, verifie sur `main`).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

* Fix(genai,#23264): le repli autonome de cle suit l'ordre par fournisseur

Le carnet 02_PromptEngineering resolvait sa cle par emplacement
(env OPENAI, env OPENROUTER, Colab OPENAI, Colab OPENROUTER) alors que
l'organe de la serie (shared/helpers/genai_helpers.py, resolve_openai_config)
resout par fournisseur (env OPENAI, Colab OPENAI, env OPENROUTER, Colab
OPENROUTER). Avec une cle OPENROUTER_API_KEY dans l'environnement ET un
Secret Colab OPENAI_API_KEY, le carnet retenait openrouter et l'organe
openai : le meme carnet, deux fournisseurs, selon que l'organe soit
importable ou non.

Le repli est realigne sur le contrat commun, et une sonde hors reseau
(genai/shared/helpers/tests/test_colab_config_contract.py) extrait la
fonction du carnet par AST depuis la cellule commitee et verifie le
fournisseur et la source pour six cas, dont le cas croise qui discriminait
les deux ordres. La sonde ne recopie pas le contrat dans le test et
n'imprime aucune valeur de cle.

Le carnet est re-execute bout en bout (25/25 cellules, 0 echec) et les
sorties reelles sont commitees. La ligne de traduction de la cellule
modifiee est rafraichie dans translations/genai/texte.csv.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

* Fix(genai,#23264): la re-execution complete ne rafraichit que la cellule corrigee

Le rejeu bout en bout du carnet (25/25 cellules) avait rafraichi les
sorties de 17 cellules, dont des appels LLM dont la reponse est
non deterministe : le texte des reponses change a chaque execution. La
cellule `9d54be4e` (Self-refine Step 2) sortait donc avec une reponse
differente de celle du merge-base, et la garde de derive de kernel
(base vs PR) y voyait une derive de signature : `1 code cells show
float-array repr drift`.

La correction porte sur une seule cellule (la resolution de la cle) :
le livrable ne doit pas rafraichir les sorties des cellules qu'il ne
touche pas. Les sorties des 17 cellules sont donc restaurees a leur
etat commite, et seule la source de la cellule `7b9f43da` porte le
correctif -- sa sortie est identique byte pour byte entre les deux
executions, la cellule n'ayant appele aucun modele.

Mesures apres restauration : sorties differentes = 0, metadata differentes
= 0, une seule source differente (`7b9f43da`), 25 cellules de code sans
`execution_count` nul, `nbformat` valide. Le rejeu complet reste prouve
(25/25 cellules, 0 echec) ; il est decrit dans le body de la PR, il
n'est pas le contenu du diff.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

* Fix(genai,#23288): un import casse de l'organe cesse d'etre un skip

`_organe()` faisait `try: import genai_helpers / except Exception: return None`.
Un module ABSENT du checkout et un module PRESENT dont l'import LEVE
produisaient donc le meme `None`, donc le meme skip : le test de parite ne
pouvait pas distinguer « composition #23269 pas faite » d'« import casse »,
et un import casse restait invisible derriere un skip.

La borne : `find_spec` tranche l'absence SANS importer ; `import_module` est
appele ensuite et laisse remonter tout ce qui casse. Le skip porte desormais
son motif -- absence du module, ou module present mais sans
`resolve_openai_config` -- deux etats que le compte de skips confondait.

Controles, dans les deux sens (sans eux la borne ne prouverait rien) :
- `test_absence_de_l_organe_rend_none` : absence -> None ;
- `test_un_import_casse_fait_echouer_au_lieu_de_skipper` : un faux
  `genai_helpers` qui leve a l'import fait echouer, il ne skippe pas.

Mesure (helpers/tests, python 3.14 / pytest 9.1.1) : 111 passed, 6 skipped.
Les 6 skips sont les cas de parite ; avec le chemin `shared/helpers` sur
`PYTHONPATH`, le motif change (« present mais sans resolve_openai_config »)
mais le compte ne bouge pas : `def resolve_openai_config` n'existe encore
nulle part dans le depot, ni sur cette branche ni sur origin/main. La moitie
« parite » du contrat reste donc non verifiable jusqu'a livraison de #23269 --
ce que le test dit maintenant au lieu de le taire.

Accessoirement : `FICHIers_TEST` (ligne 53) etait un NameError en attente,
`FICHIER_TEST` etant reference ligne 61 ; renomme.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants