Skip to content
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies = [
"fastapi>=0.104.1",
"uvicorn>=0.24.0",
"requests>=2.31.0",
"httpx>=0.24.0",
"python-dotenv>=1.0.0",
"APScheduler>=3.10.4",
"psycopg2-binary>=2.9.9",
Expand Down
12 changes: 12 additions & 0 deletions src/app_readonly.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from src.config import Config
from src.database import Database
from src.scraper import HellDivers2Scraper
from src.claude_proxy import router as claude_router

# Configure logging
logging.basicConfig(
Expand Down Expand Up @@ -39,6 +40,9 @@ async def lifespan(app: FastAPI):
lifespan=lifespan,
)

# Claude proxy (when CLAUDE_API_KEY is set)
app.include_router(claude_router)

# Add CORS middleware
app.add_middleware(
CORSMiddleware,
Expand Down Expand Up @@ -382,6 +386,14 @@ async def get_biomes():
# ========================


@app.get("/api/config", tags=["Config"])
async def get_config():
"""Public config for UI (e.g. whether Claude is available via backend)."""
return {
"claudeEnabled": bool(Config.CLAUDE_API_KEY),
}


@app.get("/api/health", tags=["Health"])
async def health_check():
"""Health check endpoint"""
Expand Down
58 changes: 58 additions & 0 deletions src/claude_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Claude API proxy - forwards /claude/* to Anthropic API with API key from config.
The key is read from CLAUDE_API_KEY env (typically from a k8s secret).
"""
import logging
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import Response
import httpx
from src.config import Config

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/claude", tags=["Claude"])
ANTHROPIC_BASE = "https://api.anthropic.com"


@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def proxy_to_anthropic(request: Request, path: str):
"""Proxy requests to Anthropic API, adding API key from server config."""
api_key = Config.CLAUDE_API_KEY
if not api_key:
raise HTTPException(
status_code=503,
detail="Claude API key not configured. Set CLAUDE_API_KEY in environment.",
)

# Map /claude/messages -> /v1/messages
anthropic_path = f"/v1/{path}" if path else "/v1"
url = f"{ANTHROPIC_BASE}{anthropic_path}"

# Forward headers, ensure API key is set (server key takes precedence)
headers = dict(request.headers)
headers.pop("host", None)
headers["x-api-key"] = api_key
headers["anthropic-dangerous-direct-browser-access"] = "true"

try:
body = await request.body()
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.request(
method=request.method,
url=url,
headers=headers,
content=body,
)
# Forward only safe headers (exclude transfer-encoding, connection, etc.)
forward_headers = {
k: v for k, v in response.headers.items()
if k.lower() not in ("transfer-encoding", "connection", "content-encoding")
}
return Response(
content=response.content,
status_code=response.status_code,
headers=forward_headers,
)
except httpx.HTTPError as e:
logger.error(f"Claude proxy error: {e}")
raise HTTPException(status_code=502, detail=f"Upstream error: {str(e)}")
3 changes: 3 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class Config:
# Logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

# Claude API (optional - for UI Claude integration via backend proxy)
CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY", "")


class DevelopmentConfig(Config):
"""Development configuration"""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_save_war_status(self, temp_db, mock_psycopg2):

data = {"war_id": 1, "status": "active"}
result = temp_db.save_war_status(data)

# Verify save was called
assert result is True
mock_cursor.execute.assert_called()
Expand All @@ -74,7 +74,7 @@ def test_save_statistics(self, temp_db, mock_psycopg2):

data = {"total_players": 1000, "total_kills": 50000, "missions_won": 2000}
result = temp_db.save_statistics(data)

# Verify save was called
assert result is True
mock_cursor.execute.assert_called()
Expand Down