feat(llm): add LLM integration module with OpenRouter and Claude Code adapters

Implements markitect/llm/ package with concrete LLMAdapter implementations:
- OpenRouterAdapter: HTTP via urllib with retry/backoff on 429/5xx
- ClaudeCodeAdapter: subprocess-based Claude CLI with stdin piping
- Factory pattern: create_adapter("openrouter") or create_adapter("claude-code")
- API key resolution chain: constructor > env var > project-root key file
- 42 unit tests, 2 integration tests (gated on API key / CLI availability)

Also adds the infospace-with-history example with Wealth of Nations VSM
analysis pipeline, templates, schemas, source chapters, and processed
output for chapters 1-2. process_chapters.py now supports --provider
and --model flags for automatic LLM-driven processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 01:17:58 +01:00
parent 360c3b1de2
commit fecc2fd4fa
82 changed files with 43767 additions and 0 deletions

View File

View File

@@ -0,0 +1,28 @@
"""Live integration test for ClaudeCodeAdapter.
Skipped unless the ``claude`` CLI is available on PATH.
"""
import shutil
import pytest
from markitect.llm.claude_code import ClaudeCodeAdapter
from markitect.prompts.execution.models import RunConfig, LLMResponse
pytestmark = pytest.mark.skipif(
shutil.which("claude") is None,
reason="claude CLI not found on PATH",
)
class TestClaudeCodeLive:
def test_simple_completion(self):
adapter = ClaudeCodeAdapter()
config = RunConfig(timeout_seconds=60, max_tokens=50)
resp = adapter.execute_prompt("Reply with exactly: PONG", config)
assert isinstance(resp, LLMResponse)
assert len(resp.content.strip()) > 0
assert resp.metadata["provider"] == "claude-code"

View File

@@ -0,0 +1,40 @@
"""Live integration test for OpenRouterAdapter.
Skipped unless an API key is available (env var or project key file).
"""
import os
import pytest
from markitect.llm.openrouter import OpenRouterAdapter
from markitect.llm.config import resolve_api_key, find_project_root
from markitect.prompts.execution.models import RunConfig, LLMResponse
def _get_api_key():
root = find_project_root()
paths = [root / "apikey-openrouter.txt"] if root else []
return resolve_api_key(env_var="OPENROUTER_API_KEY", key_file_paths=paths)
pytestmark = pytest.mark.skipif(
_get_api_key() is None,
reason="No OpenRouter API key available",
)
class TestOpenRouterLive:
def test_simple_completion(self):
adapter = OpenRouterAdapter(
model="anthropic/claude-sonnet-4",
api_key=_get_api_key(),
)
config = RunConfig(temperature=0.0, max_tokens=50)
resp = adapter.execute_prompt("Reply with exactly: PONG", config)
assert isinstance(resp, LLMResponse)
assert len(resp.content) > 0
assert resp.usage["total_tokens"] > 0
assert resp.finish_reason in ("stop", "length")
assert resp.metadata["provider"] == "openrouter"