feat(llm): extract adapter layer for standalone llm-connect package (S1+S2)
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled

Stage 1 — Decouple:
- Move RunConfig + LLMResponse to markitect/llm/models.py (canonical)
- Move LLMAdapter + Mock/ErrorLLMAdapter to markitect/llm/adapter.py
- markitect/prompts/execution/models.py and llm_adapter.py become re-export shims
- All 4 adapters + factory.py updated to import from markitect.llm.*
- Parameterize app_name in toml_config.py (resolve_llm, get_default_layers,
  get_preference_layers): paths and env var now derived from app_name arg
- Add tests/test_llm_isolation.py: 7 isolation + backward-compat tests

Stage 2 — Extract:
- Standalone llm-connect package created at ~/llm-connect/
- All 18 llm files copied; markitect.* imports replaced with llm_connect.*
- LLMError base inlined in llm_connect/exceptions.py (no markitect dep)
- llm-connect installed into markitect-venv; declared in pyproject.toml

Smoke test: markitect llm-check succeeds (live Gemini API call).
Backward compat: markitect.prompts.execution.{models,llm_adapter} still work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 08:04:50 +01:00
parent 72b87fd82e
commit 36c20f37d0
13 changed files with 485 additions and 268 deletions

View File

@@ -28,13 +28,28 @@ from markitect.llm.config import find_project_root
HARDCODED_PROVIDER = "gemini"
HARDCODED_MODEL = "gemini-2.5-flash"
MODEL_ENV_VAR = "MARKITECT_HELPER_MODEL"
# Default (markitect) values kept for backward compatibility.
MODEL_ENV_VAR = "MARKITECT_HELPER_MODEL"
USER_CONFIG_DIR = Path.home() / ".config" / "markitect"
USER_CONFIG_PATH = USER_CONFIG_DIR / "config.toml"
DIR_CONFIG_NAME = ".markitect.toml"
# ── App-name helpers ───────────────────────────────────────────────────────
def _model_env_var(app_name: str) -> str:
return f"{app_name.upper()}_HELPER_MODEL"
def _user_config_path(app_name: str) -> Path:
return Path.home() / ".config" / app_name / "config.toml"
def _dir_config_name(app_name: str) -> str:
return f".{app_name}.toml"
# ── Data classes ──────────────────────────────────────────────────────────
@dataclass
@@ -114,11 +129,11 @@ def _clear_llm_section(path: Path, section: str) -> bool:
# ── Directory config path helper ─────────────────────────────────────────
def _dir_config_path() -> Optional[Path]:
def _dir_config_path(app_name: str = "markitect") -> Optional[Path]:
root = find_project_root()
if root is None:
return None
return root / DIR_CONFIG_NAME
return root / _dir_config_name(app_name)
# ── Resolution ───────────────────────────────────────────────────────────
@@ -126,13 +141,23 @@ def _dir_config_path() -> Optional[Path]:
def resolve_llm(
cli_provider: Optional[str] = None,
cli_model: Optional[str] = None,
app_name: str = "markitect",
) -> ResolvedLLM:
"""Walk the 7-level priority chain and return a fully resolved config.
Provider and model are resolved independently — each takes the value
from its highest-priority source.
Args:
cli_provider: Provider override from CLI.
cli_model: Model override from CLI.
app_name: Application name used to derive config paths and the
env-var prefix (e.g. ``"railiance"`` → ``RAILIANCE_HELPER_MODEL``
and ``~/.config/railiance/config.toml``).
"""
dir_path = _dir_config_path()
dir_path = _dir_config_path(app_name)
user_cfg = _user_config_path(app_name)
env_var = _model_env_var(app_name)
# Build the layers (highest priority first).
layers: list[tuple[str, LLMLayer]] = []
@@ -141,13 +166,13 @@ def resolve_llm(
layers.append(("CLI flag", LLMLayer(provider=cli_provider, model=cli_model)))
# 2. Env var (model only)
env_model = os.environ.get(MODEL_ENV_VAR) or None
layers.append(("env MARKITECT_HELPER_MODEL", LLMLayer(model=env_model)))
env_model = os.environ.get(env_var) or None
layers.append((f"env {env_var}", LLMLayer(model=env_model)))
# 3. User preference
layers.append((
"user preference",
_read_llm_section(USER_CONFIG_PATH, "preference"),
_read_llm_section(user_cfg, "preference"),
))
# 4. Directory preference
@@ -167,7 +192,7 @@ def resolve_llm(
# 6. User default
layers.append((
"user default",
_read_llm_section(USER_CONFIG_PATH, "default"),
_read_llm_section(user_cfg, "default"),
))
# 7. Hardcoded
@@ -199,20 +224,22 @@ def resolve_llm(
)
def get_default_layers() -> list[tuple[str, LLMLayer]]:
def get_default_layers(app_name: str = "markitect") -> list[tuple[str, LLMLayer]]:
"""Return only the default layers for display."""
dir_path = _dir_config_path()
dir_path = _dir_config_path(app_name)
user_cfg = _user_config_path(app_name)
dir_cfg_name = _dir_config_name(app_name)
layers: list[tuple[str, LLMLayer]] = []
if dir_path:
layers.append((
f"Directory default ({DIR_CONFIG_NAME})",
f"Directory default ({dir_cfg_name})",
_read_llm_section(dir_path, "default"),
))
layers.append((
f"User default ({USER_CONFIG_PATH})",
_read_llm_section(USER_CONFIG_PATH, "default"),
f"User default ({user_cfg})",
_read_llm_section(user_cfg, "default"),
))
layers.append((
@@ -223,19 +250,21 @@ def get_default_layers() -> list[tuple[str, LLMLayer]]:
return layers
def get_preference_layers() -> list[tuple[str, LLMLayer]]:
def get_preference_layers(app_name: str = "markitect") -> list[tuple[str, LLMLayer]]:
"""Return only the preference layers for display."""
dir_path = _dir_config_path()
dir_path = _dir_config_path(app_name)
user_cfg = _user_config_path(app_name)
dir_cfg_name = _dir_config_name(app_name)
layers: list[tuple[str, LLMLayer]] = []
layers.append((
f"User preference ({USER_CONFIG_PATH})",
_read_llm_section(USER_CONFIG_PATH, "preference"),
f"User preference ({user_cfg})",
_read_llm_section(user_cfg, "preference"),
))
if dir_path:
layers.append((
f"Directory preference ({DIR_CONFIG_NAME})",
f"Directory preference ({dir_cfg_name})",
_read_llm_section(dir_path, "preference"),
))