From 3defdf99604d2bc328ef01b12a88d7232184b723 Mon Sep 17 00:00:00 2001 From: jsboige Date: Thu, 8 Oct 2026 20:58:07 +0200 Subject: [PATCH 1/2] Fix(config,#14755): ADK -- identifiant OpenRouter retire du catalogue, defauts lies Le defaut OpenRouter de Track2-GoogleADK pointait anthropic/claude-3.5-sonnet, un identifiant qui n'existe plus chez le fournisseur. Mesure firsthand contre l'API au moment du fix : 404 "No endpoints found for anthropic/claude-3.5-sonnet" sur 467 modeles au catalogue. Le defaut etait un repli jamais exerce -- get_provider_config resout `settings.openrouter_model or defaults["model"]`, et Settings.openrouter_model porte une valeur non vide, donc DEFAULTS n'est lu que si la variable d'environnement est videe. Mais les deux valeurs avaient divergE sans bruit : providers.py:48 disait claude-3.5-sonnet, providers.py:90 disait openai/gpt-5, et les deux gabarits .env.example un troisieme etat. Rien ne les liait. Cible retenue : openai/gpt-5.6-sol, verifiee presente au catalogue du fournisseur, et coherente avec le defaut OpenAI du meme fichier (gpt-5.6-sol) ainsi qu'avec le mandat #14755 (generation 5.6). - providers.py: DEFAULTS[OPENROUTER] et Settings.openrouter_model alignes, avec le commentaire qui dit pourquoi ils doivent le rester. - .env.example et config/.env.example : le modele OpenRouter suit, y compris la ligne commentee du second gabarit qui portait encore le vieil identifiant. - test_providers.py: fixture d'echappement litellm mise a jour, et test neuf `test_settings_model_matches_provider_default_for_each_provider` qui lie Settings et DEFAULTS pour les six providers -- c'est l'invariant dont l'absence a laisse les deux valeurs deriver. Preuves. Suite du module : 31 passed in 0.17s. Chemin OpenRouter reel exerce via le code du depot (get_provider_config -> get_litellm_model -> litellm) : modele servi openai/gpt-5.6-sol, reponse 'OK', usage 12+5=17 tokens. Controle negatif : l'ancien identifiant rend NotFoundError 404. Budget mesure avant appel : 212,89 credits restants sur 400. Aucun notebook touche : les sorties committee de la serie sont des comptes rendus de runs anterieurs (elles citent openai/gpt-4.1-mini), pas de la configuration, et aucune cellule code ne change -- donc aucune re-execution dans ce lot, conformement a la consigne du coordinateur. Co-Authored-By: Claude Sonnet 5.5 --- .../Track2-GoogleADK/.env.example | 2 +- .../Track2-GoogleADK/config/.env.example | 2 +- .../Track2-GoogleADK/config/providers.py | 8 ++++-- .../Track2-GoogleADK/config/test_providers.py | 26 +++++++++++++++++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/.env.example b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/.env.example index f52e0cf0c1..395b287f85 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/.env.example +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/.env.example @@ -15,7 +15,7 @@ GEMINI_MODEL=gemini-3.1-pro # ============================================================ OPENROUTER_API_KEY=your-openrouter-api-key OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 -OPENROUTER_MODEL=openai/gpt-5 +OPENROUTER_MODEL=openai/gpt-5.6-sol # ============================================================ # Qwen Cloud (API OpenAI-compatible) diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/.env.example b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/.env.example index 51f42eee94..17c9d55a1b 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/.env.example +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/.env.example @@ -18,7 +18,7 @@ OPENAI_MODEL=gpt-5.6-sol # === OPENROUTER (Accès multi-modèles) === # Utilise les mêmes variables OPENAI_* #OPENAI_BASE_URL=https://openrouter.ai/api/v1 -#OPENAI_MODEL=anthropic/claude-3.5-sonnet +#OPENAI_MODEL=openai/gpt-5.6-sol # === vLLM via Reverse Proxy === # Configurez votre endpoint vLLM hébergé diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/providers.py b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/providers.py index eb7142462f..a058bb4dcf 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/providers.py +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/providers.py @@ -45,7 +45,7 @@ class ProviderConfig(BaseModel): "base_url": "https://api.openai.com/v1" }, ProviderType.OPENROUTER: { - "model": "anthropic/claude-3.5-sonnet", + "model": "openai/gpt-5.6-sol", "base_url": "https://openrouter.ai/api/v1" }, ProviderType.QWEN: { @@ -85,9 +85,13 @@ class Settings(BaseSettings): openai_model: str = "gpt-5.6-sol" # OpenRouter (multi-modeles) + # Doit rester aligne sur ProviderConfig.DEFAULTS[OPENROUTER]["model"] : les deux + # divergent silencieusement sinon, car get_provider_config fait + # `settings.openrouter_model or defaults["model"]` et le repli n'est donc + # jamais exerce tant que la variable d'environnement porte une valeur. openrouter_api_key: Optional[str] = None openrouter_base_url: str = "https://openrouter.ai/api/v1" - openrouter_model: str = "openai/gpt-5" + openrouter_model: str = "openai/gpt-5.6-sol" # Qwen Cloud (API OpenAI-compatible) qwen_api_key: Optional[str] = None diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py index 753ad4be03..1b8ccafadc 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py @@ -120,6 +120,28 @@ def test_settings_has_default_model_for_each_provider(): assert s.lmstudio_model +def test_settings_model_matches_provider_default_for_each_provider(): + # `get_provider_config` resout `settings.

_model or defaults["model"]` : le + # repli DEFAULTS n'est donc exerce que si la variable d'environnement est vide. + # Les deux valeurs divergent alors sans bruit, et le repli mort se perime sans + # que rien ne rougisse (c'est ainsi que le defaut OpenRouter a garde un + # identifiant retire du catalogue). Ce test les lie. + s = Settings() + paires = [ + (ProviderType.GEMINI, s.gemini_model), + (ProviderType.OPENAI, s.openai_model), + (ProviderType.OPENROUTER, s.openrouter_model), + (ProviderType.QWEN, s.qwen_model), + (ProviderType.VLLM, s.vllm_model), + (ProviderType.LMSTUDIO, s.lmstudio_model), + ] + for provider, valeur_settings in paires: + assert valeur_settings == ProviderConfig.get_defaults(provider)["model"], ( + f"Settings et DEFAULTS divergent pour {provider.value} : " + f"{valeur_settings!r} != {ProviderConfig.get_defaults(provider)['model']!r}" + ) + + def test_settings_active_provider_is_case_insensitive_in_routing(): # get_provider_config lower-cases active_provider before the enum lookup. s = Settings(active_provider="GEMINI") @@ -230,8 +252,8 @@ def test_get_litellm_model_gemini_prefix(): def test_get_litellm_model_openrouter_prefix(): - cfg = ProviderConfig(provider=ProviderType.OPENROUTER, model="anthropic/claude-3.5-sonnet") - assert providers.get_litellm_model(cfg) == "openrouter/anthropic/claude-3.5-sonnet" + cfg = ProviderConfig(provider=ProviderType.OPENROUTER, model="openai/gpt-5.6-sol") + assert providers.get_litellm_model(cfg) == "openrouter/openai/gpt-5.6-sol" @pytest.mark.parametrize("provider,model,prefix", [ From 3c4c42b770f4aa2fecbf04768278fb727032759d Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 9 Oct 2026 01:28:11 +0200 Subject: [PATCH 2/2] Fix(test,#19972): hermeticite du controle defauts Settings -- lire les defauts declares Le test introduit comparait une instance `Settings()` (qui resout l'environnement et le `.env`) aux DEFAULTS du catalogue : une surcharge externe rendait la comparaison fausse et pouvait masquer une divergence litterale -- exactement le defaut releve par la review 5463813094 de #19972. - les assertions de defauts portent desormais sur `Settings.model_fields[...].default` (valeur DECLAREE), jamais sur une instance nue ; - helper `_divergences_defauts_declares()` partage par le controle et son temoin ; - temoin negatif ajoute : sous `OPENROUTER_MODEL=` ET un defaut declare divergent, la lecture sur instance reste muette tandis que le controle sur defauts declares rougit pour OpenRouter. 32 tests passent (environnement propre ; OPENROUTER_MODEL=anthropic/claude-3.5-sonnet ; surcharge vide). Aucun appel payant, aucune re-execution de notebook. Co-Authored-By: Claude Sonnet 5.5 --- .../Track2-GoogleADK/config/test_providers.py | 94 ++++++++++++++----- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py index 1b8ccafadc..8b1632ea74 100644 --- a/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py +++ b/MyIA.AI.Notebooks/ML/DataScienceWithAgents/Track2-GoogleADK/config/test_providers.py @@ -13,7 +13,11 @@ No `.env` is required: `Settings` is constructed with explicit field overrides (`Settings(active_provider="gemini", ...)`) so the tests are deterministic and -do not depend on the host environment. +do not depend on the host environment. Les assertions qui portent sur les +*valeurs par defaut* lisent les defauts DECLARES (`Settings.model_fields`), jamais +une instance `Settings()` nue : une instance resout les variables d'environnement +et le `.env`, si bien qu'une surcharge externe masquerait une divergence litterale +(review 5463813094 de #19972). """ from __future__ import annotations @@ -104,20 +108,52 @@ def test_provider_config_is_constructible_with_minimal_fields(): def test_settings_default_active_provider_is_vllm(): - s = Settings() - assert s.active_provider == "vllm" + # Defaut DECLARE, pas la valeur resolue : une instance Settings() lit le .env + # et l'environnement, ou ACTIVE_PROVIDER surchargerait l'assertion. + assert Settings.model_fields["active_provider"].default == "vllm" def test_settings_has_default_model_for_each_provider(): - s = Settings() # Each provider branch in get_provider_config reads settings.

_model; - # assert the defaults are non-empty strings so `settings.X or defaults["model"]` - # always resolves to a real model name. - assert s.gemini_model - assert s.openai_model - assert s.openrouter_model - assert s.vllm_model - assert s.lmstudio_model + # assert the DECLARED defaults are non-empty strings so + # `settings.X or defaults["model"]` always resolves to a real model name. + # Declared, not resolved: see the module docstring on hermeticity. + for champ in ( + "gemini_model", + "openai_model", + "openrouter_model", + "vllm_model", + "lmstudio_model", + ): + valeur = Settings.model_fields[champ].default + assert isinstance(valeur, str) and valeur + + +_CHAMP_MODELE = { + ProviderType.GEMINI: "gemini_model", + ProviderType.OPENAI: "openai_model", + ProviderType.OPENROUTER: "openrouter_model", + ProviderType.QWEN: "qwen_model", + ProviderType.VLLM: "vllm_model", + ProviderType.LMSTUDIO: "lmstudio_model", +} + + +def _divergences_defauts_declares(): + """Paires (provider, defaut declare, defaut du catalogue) qui divergent. + + Lit les defauts DECLARES (`Settings.model_fields[...].default`) et non une + instance : `Settings()` resout les variables d'environnement et le `.env`, donc + une surcharge externe rendrait la comparaison aveugle a une divergence + litterale -- c'est le defaut releve par la review 5463813094. + """ + divergences = [] + for provider, champ in _CHAMP_MODELE.items(): + declare = Settings.model_fields[champ].default + catalogue = ProviderConfig.get_defaults(provider)["model"] + if declare != catalogue: + divergences.append((provider, declare, catalogue)) + return divergences def test_settings_model_matches_provider_default_for_each_provider(): @@ -125,21 +161,27 @@ def test_settings_model_matches_provider_default_for_each_provider(): # repli DEFAULTS n'est donc exerce que si la variable d'environnement est vide. # Les deux valeurs divergent alors sans bruit, et le repli mort se perime sans # que rien ne rougisse (c'est ainsi que le defaut OpenRouter a garde un - # identifiant retire du catalogue). Ce test les lie. - s = Settings() - paires = [ - (ProviderType.GEMINI, s.gemini_model), - (ProviderType.OPENAI, s.openai_model), - (ProviderType.OPENROUTER, s.openrouter_model), - (ProviderType.QWEN, s.qwen_model), - (ProviderType.VLLM, s.vllm_model), - (ProviderType.LMSTUDIO, s.lmstudio_model), - ] - for provider, valeur_settings in paires: - assert valeur_settings == ProviderConfig.get_defaults(provider)["model"], ( - f"Settings et DEFAULTS divergent pour {provider.value} : " - f"{valeur_settings!r} != {ProviderConfig.get_defaults(provider)['model']!r}" - ) + # identifiant retire du catalogue). Ce test les lie -- sur les defauts + # DECLARES, jamais sur une instance (review 5463813094). + assert _divergences_defauts_declares() == [] + + +def test_declared_defaults_check_detects_divergence_under_env_override(monkeypatch): + """Temoin negatif : une surcharge d'environnement ne masque PAS une divergence litterale. + + On force (a) la surcharge externe a la valeur du catalogue ET (b) une divergence + du defaut declare : une comparaison sur instance `Settings()` ne verrait que la + surcharge et resterait muette, le controle sur defauts declares doit rougir. + """ + catalogue = ProviderConfig.get_defaults(ProviderType.OPENROUTER)["model"] + monkeypatch.setenv("OPENROUTER_MODEL", catalogue) + champ = Settings.model_fields["openrouter_model"] + monkeypatch.setattr(champ, "default", "modele-bidon/inexistant") + # La surcharge masque bien la divergence pour une lecture sur instance : + assert Settings().openrouter_model == catalogue + # ... et le controle sur defauts declares la voit quand meme : + divergences = _divergences_defauts_declares() + assert [p for p, _, _ in divergences] == [ProviderType.OPENROUTER], divergences def test_settings_active_provider_is_case_insensitive_in_routing():