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>
95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
"""
|
|
Claude Code CLI adapter — runs the ``claude`` CLI as a subprocess.
|
|
"""
|
|
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
from markitect.llm.adapter import LLMAdapter
|
|
from markitect.llm.models import RunConfig, LLMResponse
|
|
from markitect.llm.config import LLMConfig
|
|
from markitect.llm._token_estimator import estimate_tokens
|
|
from markitect.llm.exceptions import (
|
|
LLMSubprocessError,
|
|
LLMTimeoutError,
|
|
)
|
|
|
|
|
|
class ClaudeCodeAdapter(LLMAdapter):
|
|
"""LLM adapter that shells out to the ``claude`` CLI with ``--print``.
|
|
|
|
The compiled prompt is piped via **stdin** to avoid shell argument
|
|
length limits (compiled prompts can exceed 30 KB).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
cli_path: str = "claude",
|
|
model: Optional[str] = None,
|
|
config: Optional[LLMConfig] = None,
|
|
):
|
|
self._config = config or LLMConfig(provider="claude-code")
|
|
self._cli_path = cli_path or self._config.claude_cli_path
|
|
self._model = model
|
|
|
|
# ── LLMAdapter interface ────────────────────────────────────────
|
|
|
|
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
|
|
cmd = [self._cli_path, "--print"]
|
|
if self._model:
|
|
cmd.extend(["--model", self._model])
|
|
|
|
timeout = config.timeout_seconds or self._config.timeout_seconds
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
input=prompt,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
raise LLMTimeoutError(
|
|
f"claude CLI timed out after {timeout}s",
|
|
cause=exc,
|
|
) from exc
|
|
|
|
if result.returncode != 0:
|
|
raise LLMSubprocessError(
|
|
f"claude CLI exited with code {result.returncode}",
|
|
return_code=result.returncode,
|
|
stderr=result.stderr,
|
|
)
|
|
|
|
content = result.stdout
|
|
prompt_tokens = estimate_tokens(prompt)
|
|
completion_tokens = estimate_tokens(content)
|
|
|
|
return LLMResponse(
|
|
content=content,
|
|
model=self._model or "claude-code-cli",
|
|
usage={
|
|
"prompt_tokens": prompt_tokens,
|
|
"completion_tokens": completion_tokens,
|
|
"total_tokens": prompt_tokens + completion_tokens,
|
|
},
|
|
finish_reason="stop",
|
|
metadata={
|
|
"provider": "claude-code",
|
|
"cli_path": self._cli_path,
|
|
},
|
|
)
|
|
|
|
def validate_config(self, config: RunConfig) -> bool:
|
|
try:
|
|
result = subprocess.run(
|
|
[self._cli_path, "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
return result.returncode == 0
|
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
return False
|