Skip to content

Commit 31d8be4

Browse files
committed
P5-PR14: polish — admin health alias at /admin/health-status; rate limit headers add X-RateLimit-Reset; keep single limiter mount
1 parent 947831c commit 31d8be4

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

api/app/main.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,38 @@ def health_ready():
905905
)
906906

907907

908+
@app.get("/admin/health-status", dependencies=[Depends(require_admin)])
909+
async def admin_health_status():
910+
"""Return provider health summary (alias for /admin/providers/health)."""
911+
try:
912+
from .routers.admin_providers import get_provider_health_status # type: ignore
913+
from fastapi import Request as _Req
914+
# Fabricate a minimal Request-like object with app reference
915+
class _R:
916+
def __init__(self, app):
917+
self.app = app
918+
return await get_provider_health_status(_R(app)) # type: ignore
919+
except Exception:
920+
hm = getattr(app.state, "health_monitor", None)
921+
if not hm:
922+
raise HTTPException(503, detail="Health monitor not available")
923+
statuses = await hm.get_provider_statuses()
924+
# Summarize
925+
counts = {k: 0 for k in ["healthy", "degraded", "circuit_open", "disabled"]}
926+
for s in statuses.values():
927+
st = s.get("status", "healthy")
928+
if st in counts:
929+
counts[st] += 1
930+
return {
931+
"provider_statuses": statuses,
932+
"total_providers": len(statuses),
933+
"healthy_count": counts["healthy"],
934+
"degraded_count": counts["degraded"],
935+
"circuit_open_count": counts["circuit_open"],
936+
"disabled_count": counts["disabled"],
937+
}
938+
939+
908940
def require_api_key(request: Request, x_api_key: Optional[str] = Header(default=None)):
909941
"""Authenticate request using either env API_KEY or DB-backed key.
910942
Behavior:

api/app/middleware/hierarchical_rate_limiter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,18 @@ def _rate_limit_response(self, limit: int) -> Response:
212212
"retry_after_seconds": retry_after
213213
}
214214

215+
# Compute seconds until window reset; fall back to retry_after on error
216+
try:
217+
now = int(time.time())
218+
seconds = 60 - (now % 60)
219+
except Exception:
220+
seconds = retry_after
221+
215222
headers = {
216223
"Retry-After": str(retry_after),
217224
"X-RateLimit-Limit": str(limit),
218225
"X-RateLimit-Remaining": "0",
226+
"X-RateLimit-Reset": str(seconds),
219227
}
220228

221229
return Response(
@@ -224,3 +232,12 @@ def _rate_limit_response(self, limit: int) -> Response:
224232
headers=headers,
225233
media_type="application/json"
226234
)
235+
236+
def remaining_for(self, bucket_key: str, limit: int) -> int:
237+
try:
238+
bucket = self.rate_buckets.get(bucket_key, {"count": 0})
239+
used = int(bucket.get("count", 0))
240+
remaining = max(0, limit - used)
241+
return remaining
242+
except Exception:
243+
return 0

0 commit comments

Comments
 (0)