Padmanav commited on
Commit
ae4efe3
·
1 Parent(s): 4466add

feat(api): add tiered per-endpoint rate limits (sync vs async vs poll)

Browse files
Files changed (3) hide show
  1. app/api/limiter.py +12 -2
  2. app/api/routes.py +5 -2
  3. docs/api.md +17 -1
app/api/limiter.py CHANGED
@@ -2,13 +2,23 @@ from slowapi import Limiter
2
  from slowapi.util import get_remote_address
3
  from starlette.requests import Request
4
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  def get_rate_limit_key(request: Request) -> str:
7
- """Rate limit by API key if present, fall back to IP."""
8
  api_key = request.headers.get("X-API-Key")
9
  if api_key:
10
  return f"key:{api_key}"
11
  return f"ip:{get_remote_address(request)}"
12
 
13
 
14
- limiter = Limiter(key_func=get_rate_limit_key)
 
2
  from slowapi.util import get_remote_address
3
  from starlette.requests import Request
4
 
5
+ # Tier definitions — referenced by name in route decorators
6
+ # Sync analyze: expensive full pipeline, strict limit
7
+ # Async submit: just queues a Celery task, more generous
8
+ # Job poll: read-only status check, very generous
9
+ # Health/metrics: unrestricted (no decorator applied)
10
+
11
+ LIMIT_SYNC_ANALYZE = "5/minute"
12
+ LIMIT_ASYNC_SUBMIT = "20/minute"
13
+ LIMIT_JOB_POLL = "120/minute"
14
+
15
 
16
  def get_rate_limit_key(request: Request) -> str:
17
+ """Rate limit by API key if present, fall back to IP address."""
18
  api_key = request.headers.get("X-API-Key")
19
  if api_key:
20
  return f"key:{api_key}"
21
  return f"ip:{get_remote_address(request)}"
22
 
23
 
24
+ limiter = Limiter(key_func=get_rate_limit_key)
app/api/routes.py CHANGED
@@ -15,6 +15,8 @@ from app.services.analysis_service import run_full_analysis
15
  from app.agents import repo_analysis_agent
16
  from app.models.review import ReviewFeedback
17
  from app.tools.github_tool import clone_repository, delete_repository
 
 
18
 
19
  router = APIRouter()
20
 
@@ -29,7 +31,7 @@ def health_check() -> dict[str, str]:
29
  @router.post(
30
  "/analyze", response_model=EngineeringReport, dependencies=[Depends(verify_api_key)]
31
  )
32
- @limiter.limit("5/minute")
33
  async def analyze_repository(
34
  request: Request, body: RepositoryRequest
35
  ) -> EngineeringReport:
@@ -57,7 +59,7 @@ async def analyze_repository(
57
  status_code=202,
58
  dependencies=[Depends(verify_api_key)],
59
  )
60
- @limiter.limit("5/minute")
61
  async def analyze_repository_async(
62
  request: Request, body: RepositoryRequest
63
  ) -> JobSubmitResponse:
@@ -74,6 +76,7 @@ async def analyze_repository_async(
74
  response_model=JobStatusResponse,
75
  dependencies=[Depends(verify_api_key)],
76
  )
 
77
  def get_job_status(job_id: str) -> JobStatusResponse:
78
  """Poll the status/result of an async analysis job."""
79
  result = celery_app.AsyncResult(job_id)
 
15
  from app.agents import repo_analysis_agent
16
  from app.models.review import ReviewFeedback
17
  from app.tools.github_tool import clone_repository, delete_repository
18
+ from app.api.limiter import limiter, LIMIT_SYNC_ANALYZE, LIMIT_ASYNC_SUBMIT, LIMIT_JOB_POLL
19
+
20
 
21
  router = APIRouter()
22
 
 
31
  @router.post(
32
  "/analyze", response_model=EngineeringReport, dependencies=[Depends(verify_api_key)]
33
  )
34
+ @limiter.limit(LIMIT_SYNC_ANALYZE)
35
  async def analyze_repository(
36
  request: Request, body: RepositoryRequest
37
  ) -> EngineeringReport:
 
59
  status_code=202,
60
  dependencies=[Depends(verify_api_key)],
61
  )
62
+ @limiter.limit(LIMIT_ASYNC_SUBMIT)
63
  async def analyze_repository_async(
64
  request: Request, body: RepositoryRequest
65
  ) -> JobSubmitResponse:
 
76
  response_model=JobStatusResponse,
77
  dependencies=[Depends(verify_api_key)],
78
  )
79
+ @limiter.limit(LIMIT_JOB_POLL)
80
  def get_job_status(job_id: str) -> JobStatusResponse:
81
  """Poll the status/result of an async analysis job."""
82
  result = celery_app.AsyncResult(job_id)
docs/api.md CHANGED
@@ -48,4 +48,20 @@ Request:
48
  Response: `RepositoryResponse` object
49
 
50
  ## Interactive Docs
51
- Visit http://localhost:8000/docs for Swagger UI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  Response: `RepositoryResponse` object
49
 
50
  ## Interactive Docs
51
+ Visit http://localhost:8000/docs for Swagger UI
52
+
53
+ ## Rate limits
54
+
55
+ All limits are applied per API key (via `X-API-Key` header). Requests without
56
+ a key are limited by IP address.
57
+
58
+ | Endpoint | Limit | Reason |
59
+ |---|---|---|
60
+ | `POST /api/v1/analyze` | 5 req/min | Full pipeline — LLM calls, clone, static analysis |
61
+ | `POST /api/v1/analyze/async` | 20 req/min | Queue submission only — fast and cheap |
62
+ | `GET /api/v1/jobs/{id}` | 120 req/min | Read-only status poll |
63
+ | `GET /api/v1/health` | unlimited | Healthcheck — no limit applied |
64
+ | `GET /metrics/prometheus` | unlimited | Prometheus scrape — no limit applied |
65
+
66
+ When a limit is exceeded the API returns `429 Too Many Requests` with a
67
+ `Retry-After` header indicating when the client may retry.