diff --git a/src/ai_client.py b/src/ai_client.py index 473a8f03..f7dcbe3a 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -51,8 +51,11 @@ from src.models import ToolPreset, BiasProfile, Tool from src.paths import get_credentials_path from src.tool_bias import ToolBiasEngine from src.tool_presets import ToolPresetManager +from src.tool_presets import ToolPresetManager +PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] +# _require_warmed lives # _require_warmed lives in src/module_loader.py to avoid duplicating the # lookup logic across files that need heavy modules. Re-exported here so # existing call sites and the T3.1 test (which asserts diff --git a/src/models.py b/src/models.py index 53735156..4aec68d1 100644 --- a/src/models.py +++ b/src/models.py @@ -53,7 +53,14 @@ from src.paths import get_config_path #region: Constants -PROVIDERS: List[str] = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] +# PROVIDERS is the source of truth in src/ai_client.py (per the +# follow-up track's Naming Convention HARD RULE). Lazy-loaded +# via the __getattr__ defined later in this module to break the +# circular import (src.ai_client imports ToolPreset/BiasProfile/ +# Tool from this module at line 50, so a top-level 'from +# src.ai_client import PROVIDERS' here would deadlock). The +# audit script scripts/audit_providers_source_of_truth.py +# verifies PROVIDERS is declared in src/ai_client.py and not here. AGENT_TOOL_NAMES: List[str] = [ "run_powershell", @@ -251,6 +258,9 @@ _PYDANTIC_CLASS_FACTORIES: dict[str, callable] = { } def __getattr__(name: str) -> Any: + if name == "PROVIDERS": + from src.ai_client import PROVIDERS as _PROVIDERS + return _PROVIDERS if name in _PYDANTIC_CLASS_FACTORIES: cls = _PYDANTIC_CLASS_FACTORIES[name]() globals()[name] = cls diff --git a/tests/test_provider_curation.py b/tests/test_provider_curation.py index 319c9ccc..a9948f8e 100644 --- a/tests/test_provider_curation.py +++ b/tests/test_provider_curation.py @@ -3,6 +3,6 @@ import src.app_controller def test_providers_moved_to_models(): """Verify that PROVIDERS list is in models.py and removed from AppController.""" - expected_providers = ['gemini', 'anthropic', 'gemini_cli', 'deepseek', 'minimax'] + expected_providers = ['gemini', 'anthropic', 'gemini_cli', 'deepseek', 'minimax', 'qwen', 'grok', 'llama'] assert models.PROVIDERS == expected_providers assert not hasattr(src.app_controller.AppController, 'PROVIDERS') \ No newline at end of file diff --git a/tests/test_providers_source_of_truth.py b/tests/test_providers_source_of_truth.py new file mode 100644 index 00000000..91609d1d --- /dev/null +++ b/tests/test_providers_source_of_truth.py @@ -0,0 +1,23 @@ +"""Verify PROVIDERS is defined in src.ai_client (the source of truth) +and re-exported from src.models (backward compat shim). + +Per the follow-up track's Naming Convention (HARD RULE), PROVIDERS +lives in src/ai_client.py. src/models.py keeps a re-export +shim so existing import sites don't break. +""" +from __future__ import annotations +import src.models as models +import src.ai_client as ai_client + +EXPECTED_PROVIDERS = ["gemini", "anthropic", "gemini_cli", "deepseek", "minimax", "qwen", "grok", "llama"] + +def test_providers_defined_in_src_ai_client() -> None: + assert hasattr(ai_client, "PROVIDERS") + assert ai_client.PROVIDERS == EXPECTED_PROVIDERS + +def test_providers_reexported_from_src_models() -> None: + assert hasattr(models, "PROVIDERS") + assert models.PROVIDERS == EXPECTED_PROVIDERS + +def test_providers_same_object_in_both_modules() -> None: + assert models.PROVIDERS is ai_client.PROVIDERS