import os import time from typing import List, Dict, Any, Optional from fastapi import FastAPI, HTTPException, Body, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from .config import ProxyConfig from .router import LiteLLMProxyRouter class ChatMessage(BaseModel): role: str = Field(..., description="Role of the message author (system, user, assistant)") content: str = Field(..., description="Content of the message") class ChatCompletionRequest(BaseModel): model: str = Field(..., description="The model ID to use for this request (e.g., primary-cluster)") messages: List[ChatMessage] = Field(..., description="A list of messages comprising the chat history so far") temperature: Optional[float] = Field(default=0.7, description="Sampling temperature to use") max_tokens: Optional[int] = Field(default=1000, description="The maximum number of tokens to generate") stream: Optional[bool] = Field(default=False, description="If true, stream tokens") mock_sandbox: Optional[bool] = Field(default=False, description="Force request to run in mock sandbox mode") class UIPiiConfig(BaseModel): pii_enabled: bool = Field(default=False) pii_action: str = Field(default="MASK", description="BLOCK, MASK, or REWRITE") pii_policy: Optional[Dict[str, str]] = Field(default=None) class LiteLLMProxyApp: """ Class-based FastAPI Application container for the LiteLLM Proxy microservice. Exposes an OpenAI-compatible API with intelligent routing, PII shielding, and load balancing across multiple LLM providers. The Streamlit dashboard (app.py) runs as a separate service and connects to this API via the FASTAPI_URL environment variable. """ def __init__(self, config_path: str = "config.yaml"): self.config = ProxyConfig(config_path=config_path) self.router = LiteLLMProxyRouter(config=self.config) self.app = FastAPI( title="LiteLLM Load-Balancing Routing Proxy", description=( "OOP-based Microservice for intelligent LLM routing, " "automatic fallbacks, TPM/RPM rate limiting, and DeBERTa-v3 PII shielding." ), version="2.0.0" ) # Wide-open CORS — the Streamlit service calls this API cross-origin self.app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) self._register_routes() def _register_routes(self): """Registers all FastAPI endpoints.""" # ----------------------------------------------------------- # ROOT — API info page (no Streamlit proxy needed) # ----------------------------------------------------------- @self.app.get("/", response_class=HTMLResponse) async def root(): """API landing page — links to the Streamlit dashboard service.""" streamlit_url = os.environ.get("STREAMLIT_URL", "#") return HTMLResponse(content=f""" LiteLLM Gateway API
● API Online

LiteLLM Gateway API

Intelligent LLM routing with DeBERTa PII shielding.
Connect your Streamlit dashboard to start routing queries.

Open Dashboard → API Docs
""", status_code=200) # ----------------------------------------------------------- # INFRASTRUCTURE # ----------------------------------------------------------- @self.app.get("/health") async def health(): """Health check for Railway / Kubernetes / Fly.io.""" return { "status": "healthy", "timestamp": time.time(), "routing_strategy": self.config.routing_strategy, "endpoints_loaded": len(self.config.endpoints) } @self.app.get("/metrics") async def metrics(): """Real-time load-balancing and token metrics.""" return { "timestamp": time.time(), "metrics": self.router.get_metrics(), "active_routing_rules": { "strategy": self.config.routing_strategy, "retries": self.config.num_retries, "timeout": self.config.timeout }, "registered_virtual_models": list(set(e.model_name for e in self.config.endpoints)), "registered_physical_providers": list(set(e.model.split("/")[0] for e in self.config.endpoints)) } # ----------------------------------------------------------- # LLM API ENDPOINTS # ----------------------------------------------------------- @self.app.get("/v1/models") async def list_models(): """Lists virtual and physical backend models.""" data = [] for vm in set(e.model_name for e in self.config.endpoints): data.append({ "id": vm, "object": "model", "created": 1686935002, "owned_by": "proxy-system", "type": "virtual" }) for ep in self.config.endpoints: data.append({ "id": ep.model, "object": "model", "created": 1686935002, "owned_by": ep.model.split("/")[0], "type": "physical", "tpm_limit": ep.tpm, "rpm_limit": ep.rpm, "tpr_limit": ep.tpr }) return {"object": "list", "data": data} @self.app.post("/v1/chat/completions") async def chat_completions(request: ChatCompletionRequest): """OpenAI-compatible chat completion with load balancing and PII shielding.""" messages_dict = [{"role": m.role, "content": m.content} for m in request.messages] virtual_models = set(e.model_name for e in self.config.endpoints) if request.model not in virtual_models: raise HTTPException( status_code=404, detail=f"Model '{request.model}' not found. Available: {list(virtual_models)}" ) try: return await self.router.execute_chat_completion( model=request.model, messages=messages_dict, temperature=request.temperature, max_tokens=request.max_tokens, mock_sandbox=request.mock_sandbox ) except ValueError as ve: raise HTTPException(status_code=400, detail=str(ve)) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ----------------------------------------------------------- # PREFERENCE / CREDIT ROUTING ENDPOINTS # ----------------------------------------------------------- @self.app.get("/preference-config") async def get_preference_config(): return { "preference_enabled": self.router.preference_enabled, "preference_list": self.router.preference_list, "credit_limits": self.router.credit_limits, "accumulated_spend": self.router.accumulated_spend, "available_physical_models": list(set(e.model for e in self.config.endpoints)) } @self.app.post("/preference-config") async def update_preference_config(config: dict = Body(...)): self.router.preference_enabled = config.get("preference_enabled", self.router.preference_enabled) self.router.preference_list = config.get("preference_list", self.router.preference_list) for m, limit in config.get("credit_limits", {}).items(): self.router.credit_limits[m] = float(limit) return {"status": "success", "message": "Preference routing configuration synchronized."} @self.app.post("/preference-config/reset") async def reset_preference_spend(): for m in list(self.router.accumulated_spend.keys()): self.router.accumulated_spend[m] = 0.0 return {"status": "success", "message": "Spend counters reset to zero."} # ----------------------------------------------------------- # PII GUARDRAIL CONFIGURATION ENDPOINTS # ----------------------------------------------------------- @self.app.get("/ui/pii-config") async def get_pii_config(): return { "pii_enabled": getattr(self.router, "pii_enabled", False), "pii_action": getattr(self.router, "pii_action", "MASK"), "pii_policy": getattr(self.router, "pii_policy", None) } @self.app.post("/ui/pii-config") async def update_pii_config(config: UIPiiConfig): self.router.pii_enabled = config.pii_enabled self.router.pii_action = config.pii_action self.router.pii_policy = config.pii_policy return { "status": "success", "message": f"PII Guardrail updated: enabled={config.pii_enabled}, action={config.pii_action}." } def get_app(self) -> FastAPI: """Returns the FastAPI instance for use with ASGI servers.""" return self.app