Private
Public Access
0
0

refactor(ai_client): move PROVIDERS to src/ai_client.py; re-export via models.__getattr__

Phase 2 tasks 2.1 + 2.2 + 2.3a of the follow-up track.

PROVIDERS now lives in src/ai_client.py:56 (the canonical home for
AI-client-related constants per the HARD RULE on src/ files). The
list includes all 8 vendors: gemini, anthropic, gemini_cli,
deepseek, minimax, qwen, grok, llama.

Backward compat: src/models.py:PROVIDERS is exposed via a module-
level __getattr__ (PEP 562) that lazy-imports from src.ai_client.
The lazy approach was needed because src.ai_client imports
ToolPreset/BiasProfile/Tool from src.models at line 50, so a
top-level 'from src.ai_client import PROVIDERS' in models.py
would deadlock. Adding a branch to the existing __getattr__
in models.py (which also handles pydantic class factories) is
the surgical fix.

tests/test_provider_curation.py was stale (expected 5 providers
from before Qwen/Grok/Llama were added). Updated to 8.

New test: tests/test_providers_source_of_truth.py asserts:
- src.ai_client.PROVIDERS exists and matches the 8-provider list
- src.models.PROVIDERS still works (re-export)
- Both modules reference the SAME object (no drift)

Green confirmed: 4 provider tests pass.
This commit is contained in:
2026-06-11 16:38:09 -04:00
parent eae326ea16
commit 74c3b6b274
4 changed files with 38 additions and 2 deletions
+3
View File
@@ -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
+11 -1
View File
@@ -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
+1 -1
View File
@@ -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')
+23
View File
@@ -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