Spaces:
Running
Running
| """ | |
| Proxy Routes β /v1/proxy/{provider} | |
| True firewall endpoint. Receives LLM API requests, classifies, | |
| and either blocks (403) or forwards to the LLM provider. | |
| """ | |
| import logging | |
| from datetime import datetime, timezone | |
| import httpx | |
| from fastapi import APIRouter, Depends, Request, HTTPException | |
| from fastapi.responses import StreamingResponse, JSONResponse | |
| from src.api.middleware import validate_api_key | |
| from src.utils.hashing import hash_prompt | |
| from src.db import mongo | |
| logger = logging.getLogger("llm_firewall.routes.proxy") | |
| router = APIRouter(prefix="/v1/proxy", tags=["proxy"]) | |
| SUPPORTED_PROVIDERS = {"openai", "gemini", "anthropic", "groq"} | |
| async def proxy_llm_request( | |
| provider: str, | |
| request: Request, | |
| key_doc: dict = Depends(validate_api_key), | |
| ): | |
| """ | |
| True firewall proxy endpoint. | |
| Receives a full LLM API request, classifies the prompt, | |
| and either blocks (403 + threat report) or forwards to the | |
| provider and streams the response back. | |
| Headers required: | |
| X-API-Key: firewall API key | |
| X-LLM-API-Key: the provider's API key (forwarded to LLM) | |
| """ | |
| # Validate provider | |
| if provider not in SUPPORTED_PROVIDERS: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Unsupported provider: {provider}. Supported: {', '.join(SUPPORTED_PROVIDERS)}", | |
| ) | |
| # Get LLM API key | |
| llm_api_key = request.headers.get("X-LLM-API-Key") | |
| if not llm_api_key: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="X-LLM-API-Key header required for proxy mode", | |
| ) | |
| # Parse request body | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| raise HTTPException(status_code=400, detail="Invalid JSON body") | |
| # Extract prompt from provider-specific format | |
| proxy_engine = request.app.state.proxy_engine | |
| prompt = proxy_engine.extract_prompt(provider, body) | |
| if not prompt: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Could not extract prompt from request body", | |
| ) | |
| # ββ Run classifier pipeline ββββββββββββββββββββββββββββββ | |
| pipeline = request.app.state.pipeline | |
| app_ctx = key_doc.get("app_context", "general") | |
| canary_token = key_doc.get("custom_canary", None) | |
| result = pipeline.classify( | |
| text=prompt, | |
| app_context=app_ctx, | |
| custom_canary=canary_token, | |
| use_openai_moderation=key_doc.get("use_openai_moderation", False) | |
| ) | |
| # Extract model name from body | |
| model_name = body.get("model", None) | |
| # Common firewall headers | |
| firewall_headers = { | |
| "X-Firewall-Safe": str(result.safe).lower(), | |
| "X-Firewall-Risk-Score": str(result.risk_score), | |
| "X-Firewall-Processing-Ms": str(result.processing_time_ms), | |
| } | |
| # Log the request | |
| log_entry = { | |
| "request_id": result.request_id, | |
| "api_key_id": str(key_doc["_id"]), | |
| "timestamp": datetime.now(timezone.utc), | |
| "prompt_hash": hash_prompt(prompt), | |
| "prompt_length": len(prompt), | |
| "safe": result.safe, | |
| "risk_score": result.risk_score, | |
| "attack_type": result.attack_type, | |
| "confidence": result.confidence, | |
| "flagged_layer": result.flagged_layer, | |
| "flagged_pattern": result.flagged_pattern, | |
| "provider": provider, | |
| "model": model_name, | |
| "blocked": not result.safe, | |
| "layers": result.layers, | |
| "processing_time_ms": result.processing_time_ms, | |
| "model_version": result.model_version, | |
| "metadata": {}, | |
| } | |
| try: | |
| await mongo.get_logs_collection().insert_one(log_entry) | |
| except Exception as e: | |
| logger.error(f"Failed to log proxy result: {e}") | |
| # Update key stats | |
| try: | |
| update = {"$inc": {"total_checks": 1, "monthly_usage": 1}} | |
| if not result.safe: | |
| update["$inc"]["total_blocked"] = 1 | |
| await mongo.get_keys_collection().update_one( | |
| {"_id": key_doc["_id"]}, update | |
| ) | |
| except Exception as e: | |
| logger.error(f"Failed to update key stats: {e}") | |
| # ββ BLOCK if flagged βββββββββββββββββββββββββββββββββββββ | |
| if not result.safe: | |
| logger.warning( | |
| f"BLOCKED {provider} request: {result.attack_type} " | |
| f"(risk={result.risk_score}, layer={result.flagged_layer})" | |
| ) | |
| return JSONResponse( | |
| status_code=403, | |
| content={ | |
| "error": "prompt_blocked", | |
| "firewall_report": { | |
| "risk_score": result.risk_score, | |
| "attack_type": result.attack_type, | |
| "confidence": result.confidence, | |
| "flagged_layer": result.flagged_layer, | |
| "flagged_pattern": result.flagged_pattern, | |
| "layers": result.layers, | |
| "processing_time_ms": result.processing_time_ms, | |
| "request_id": result.request_id, | |
| "timestamp": result.timestamp, | |
| }, | |
| }, | |
| headers=firewall_headers, | |
| ) | |
| # ββ FORWARD if safe ββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| is_stream = body.get("stream", False) | |
| if is_stream: | |
| response = await proxy_engine.forward_request( | |
| provider=provider, | |
| body=body, | |
| llm_api_key=llm_api_key, | |
| stream=True, | |
| ) | |
| return StreamingResponse( | |
| proxy_engine.stream_response(response), | |
| status_code=response.status_code, | |
| media_type=response.headers.get("content-type", "text/event-stream"), | |
| headers={ | |
| **firewall_headers, | |
| **{k: v for k, v in response.headers.items() | |
| if k.lower() not in ("content-length", "transfer-encoding", "content-encoding")}, | |
| }, | |
| ) | |
| else: | |
| response = await proxy_engine.forward_request( | |
| provider=provider, | |
| body=body, | |
| llm_api_key=llm_api_key, | |
| stream=False, | |
| ) | |
| return JSONResponse( | |
| status_code=response.status_code, | |
| content=response.json(), | |
| headers=firewall_headers, | |
| ) | |
| except httpx.ConnectError: | |
| raise HTTPException( | |
| status_code=502, | |
| detail=f"LLM provider '{provider}' is unreachable", | |
| ) | |
| except httpx.TimeoutException: | |
| raise HTTPException( | |
| status_code=502, | |
| detail=f"LLM provider '{provider}' timed out", | |
| ) | |
| except Exception as e: | |
| logger.error(f"Proxy forward error: {e}") | |
| raise HTTPException( | |
| status_code=502, | |
| detail=f"Error forwarding to {provider}: {str(e)[:200]}", | |
| ) | |