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:
0
tests/integration/llm/__init__.py
Normal file
0
tests/integration/llm/__init__.py
Normal file
28
tests/integration/llm/test_claude_code_live.py
Normal file
28
tests/integration/llm/test_claude_code_live.py
Normal 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"
|
||||
40
tests/integration/llm/test_openrouter_live.py
Normal file
40
tests/integration/llm/test_openrouter_live.py
Normal 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"
|
||||
Reference in New Issue
Block a user