5e763a2e37
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.3 KiB
5.3 KiB
OpusAgent
Self-hosted REST API proxy to Claude models via Claude Code SDK, leveraging a Claude Pro/Max subscription.
Architecture
Two Docker containers behind Traefik reverse proxy:
Client → Traefik (HTTPS) → FastAPI (Python, :8000) ↔ Node.js Worker (:3001, internal)
↓ ↓
SQLite (WAL) Claude Code SDK
- fastapi-service: API REST, auth, rate limiting, async queue processing
- claude-worker: Express server wrapping
@anthropic-ai/claude-codeSDK
Key Paths
| Area | Path |
|---|---|
| FastAPI entrypoint | src/backend/main.py |
| Config (Pydantic Settings) | src/backend/config.py |
| Database layer (aiosqlite) | src/backend/db/database.py |
| API models + envelope | src/backend/models/api.py |
| Routes | src/backend/api/routes/{health,topics,requests,sessions}.py |
| Auth + rate limiter | src/backend/api/dependencies.py |
| Worker HTTP client | src/backend/services/worker_client.py |
| Queue processor | src/backend/services/queue.py |
| Node.js worker | worker/index.js |
| Tests | tests/ |
| Design spec (Italian) | docs/superpowers/specs/ |
| Implementation plan | docs/superpowers/plans/ |
Tech Stack
- Python 3.11+: FastAPI, uvicorn, aiosqlite, httpx, slowapi, pydantic-settings
- Node.js 20: Express, @anthropic-ai/claude-code SDK (v1.0.128+, async iterable API)
- Package manager: uv (Python), npm (Node.js)
- Database: SQLite with WAL mode, foreign keys enabled
- Infra: Docker Compose, Traefik v3 (TLS via Let's Encrypt)
Database Schema
Three tables in SQLite:
- topics: id (UUID), name (unique), system_prompt, summary, model (nullable), timestamps
- requests: id (UUID), session_id, sdk_conversation_id, topic_id (FK), prompt, result, error, model, summarize (bool), status (pending/processing/completed/failed), timestamps
- closed_sessions: session_id (PK), closed_at
API Endpoints
All require X-Api-Key header. All responses use envelope: {success, data, error}.
GET /api/health— health check (DB + worker)GET /api/models— list available Claude models (aliases + default)POST|GET /api/topics,GET|PUT|DELETE /api/topics/{id}— topic CRUDPOST /api/topics/{id}/summarize— regenerate complete topic summaryPOST /api/requests(202) — submit prompt (async queue),GET|DELETE /api/requests[/{id}]GET /api/sessions,DELETE /api/sessions/{id}— session management
Processing Flow
POST /api/requests→ 202 Accepted (queued)- Background loop picks oldest pending, calls worker
POST /process - Worker runs Claude Code SDK (
query()async iterable) with optional session resume - Result stored in DB; SDK conversation ID saved in
sdk_conversation_id; optional incremental summary appended to topic - Client polls
GET /api/requests/{id}for result
Model resolution: request-level > topic-level > CLAUDE_MODEL env var.
Multi-turn sessions
session_id: "new"→ stored as NULL; after completion, set to SDK's conversation ID- Custom
session_id→ preserved as-is; SDK conversation ID stored separately insdk_conversation_id - Resume logic: queue looks up
sdk_conversation_idfrom previous completed requests in the same session; only passes it to worker if found (avoids resume on non-existent SDK sessions)
Commands
# Development
uv sync --all-groups
uv run dev # uvicorn with --reload
# Production
docker compose up -d # both services
docker compose exec claude-worker npx claude login # first-time auth (must run on active container, NOT run --rm)
# Tests
uv run pytest -v
# Logs
docker compose logs -f fastapi-service
docker compose logs -f claude-worker
Environment Variables (.env)
| Variable | Default | Purpose |
|---|---|---|
API_KEY |
(required) | Auth key for all endpoints |
API_HOST |
0.0.0.0 | Server bind address |
API_PORT |
8000 | Server port |
WORKER_URL |
http://claude-worker:3001 | Internal worker address |
DB_PATH |
/data/opus-agent.db | SQLite database path |
CLAUDE_MODEL |
claude-sonnet-4-6 | Default Claude model |
RATE_LIMIT_PER_MINUTE |
10 | Rate limit per API key |
RATE_LIMIT_PER_HOUR |
100 | Rate limit per API key |
Design Decisions
- Sequential processing: one request at a time (FIFO), no concurrency — simplifies Claude Code SDK session handling
- Async Python: FastAPI + aiosqlite for non-blocking I/O while queue runs in background
- SQLite + WAL: sufficient for single-writer pattern, no external DB needed
- Envelope pattern: consistent
{success, data, error}on all responses - Session = conversation: session_id maps to Claude Code SDK
resumeoption - Worker isolation: not exposed to host, only reachable from fastapi-service via Docker internal network
- Claude auth volume: credentials stored at
/root/.claude(not/home/node/.claude) because the worker process runs as root inside the container - SDK async iteration:
query()returns an async iterable (not a promise); iterate withfor await...ofand extract the message withtype === "result"
Deployment
Domain: opus-agent.tielogic.xyz → Traefik → fastapi-service:8000
Traefik config: /opt/docker/traefik/dynamic.yml (file provider, auto-reload).