Files
markitect-main/markitect/llm/_http.py
tegwick fecc2fd4fa 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>
2026-02-11 01:17:58 +01:00

80 lines
2.2 KiB
Python

"""
Thin synchronous HTTP helper built on :mod:`urllib.request`.
Translates HTTP errors into typed :mod:`markitect.llm.exceptions`.
"""
import json
import urllib.request
import urllib.error
from typing import Dict, Any, Optional
from markitect.llm.exceptions import (
LLMAPIError,
LLMRateLimitError,
LLMTimeoutError,
)
def post_json(
url: str,
payload: Dict[str, Any],
headers: Optional[Dict[str, str]] = None,
timeout: int = 300,
) -> Dict[str, Any]:
"""POST *payload* as JSON and return the parsed response body.
Raises:
LLMRateLimitError: on HTTP 429
LLMAPIError: on other non-2xx responses
LLMTimeoutError: on socket / read timeout
"""
data = json.dumps(payload).encode()
req = urllib.request.Request(
url,
data=data,
headers={"Content-Type": "application/json", **(headers or {})},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = resp.read().decode()
return json.loads(body)
except urllib.error.HTTPError as exc:
body = ""
try:
body = exc.read().decode()
except Exception:
pass
if exc.code == 429:
raise LLMRateLimitError(
f"Rate limited (429) from {url}",
status_code=429,
response_body=body,
cause=exc,
) from exc
raise LLMAPIError(
f"HTTP {exc.code} from {url}",
status_code=exc.code,
response_body=body,
cause=exc,
) from exc
except urllib.error.URLError as exc:
if "timed out" in str(exc.reason):
raise LLMTimeoutError(
f"Request to {url} timed out after {timeout}s",
cause=exc,
) from exc
raise LLMAPIError(
f"URL error for {url}: {exc.reason}",
cause=exc,
) from exc
except TimeoutError as exc:
raise LLMTimeoutError(
f"Request to {url} timed out after {timeout}s",
cause=exc,
) from exc