Spaces:
Running
Running
| import aiohttp | |
| import logging | |
| from typing import List, Dict, Optional | |
| from fastapi import FastAPI, HTTPException, Query, Path | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel | |
| from datetime import datetime, timedelta | |
| # Setup logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", | |
| datefmt="%Y-%m-%d %H:%M:%S", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # FastAPI app | |
| app = FastAPI( | |
| title="Free Models API", | |
| description="API to fetch and filter free AI models from OpenRouter", | |
| version="1.0.0" | |
| ) | |
| # Pydantic models for response structure | |
| class Architecture(BaseModel): | |
| modality: str | |
| input_modalities: List[str] | |
| output_modalities: List[str] | |
| tokenizer: str | |
| instruct_type: Optional[str] | |
| class Pricing(BaseModel): | |
| prompt: str | |
| completion: str | |
| request: str | |
| image: str | |
| audio: str | |
| web_search: str | |
| internal_reasoning: str | |
| class TopProvider(BaseModel): | |
| context_length: int | |
| max_completion_tokens: int | |
| is_moderated: bool | |
| class Model(BaseModel): | |
| id: str | |
| canonical_slug: str | |
| hugging_face_id: Optional[str] | |
| name: str | |
| created: int | |
| description: str | |
| context_length: int | |
| architecture: Architecture | |
| pricing: Pricing | |
| top_provider: TopProvider | |
| per_request_limits: Optional[Dict] | |
| supported_parameters: List[str] | |
| class FreeModelsResponse(BaseModel): | |
| success: bool | |
| message: str | |
| count: int | |
| models: List[Model] | |
| filtered_at: str | |
| # Cache for storing API responses | |
| cache = { | |
| "data": None, | |
| "timestamp": None, | |
| "ttl_minutes": 15 # Cache for 15 minutes | |
| } | |
| # Supported categories | |
| SUPPORTED_CATEGORIES = [ | |
| "chat", "reasoning", "vision", "coding", "lightweight", | |
| "roleplay", "marketing", "programming", "image", "text" | |
| ] | |
| class OpenRouterClient: | |
| """Client for interacting with OpenRouter API""" | |
| def __init__(self, api_key: Optional[str] = None): | |
| self.base_url = "https://openrouter.ai/api/v1" | |
| self.api_key = api_key | |
| self.session = None | |
| async def __aenter__(self): | |
| timeout = aiohttp.ClientTimeout(total=30) | |
| connector = aiohttp.TCPConnector(limit=10) | |
| headers = { | |
| "User-Agent": "Free-Models-API/1.0.0", | |
| "Content-Type": "application/json" | |
| } | |
| if self.api_key: | |
| headers["Authorization"] = f"Bearer {self.api_key}" | |
| self.session = aiohttp.ClientSession( | |
| timeout=timeout, | |
| connector=connector, | |
| headers=headers | |
| ) | |
| return self | |
| async def __aexit__(self, exc_type, exc_val, exc_tb): | |
| if self.session: | |
| await self.session.close() | |
| async def get_models(self, category: Optional[str] = None) -> Dict: | |
| """Fetch all available models from OpenRouter""" | |
| url = f"{self.base_url}/models" | |
| params = {} | |
| if category: | |
| params["category"] = category | |
| try: | |
| logger.info(f"Fetching models from OpenRouter API{f' with category: {category}' if category else ''}...") | |
| async with self.session.get(url, params=params) as response: | |
| if response.status != 200: | |
| logger.error(f"OpenRouter API returned status {response.status}") | |
| raise Exception(f"API request failed with status {response.status}") | |
| data = await response.json() | |
| logger.info(f"Successfully fetched {len(data.get('data', []))} models") | |
| return data | |
| except Exception as e: | |
| logger.error(f"Error fetching models: {str(e)}") | |
| raise | |
| def is_cache_valid() -> bool: | |
| """Check if cached data is still valid""" | |
| if not cache["timestamp"] or not cache["data"]: | |
| return False | |
| expiry_time = cache["timestamp"] + timedelta(minutes=cache["ttl_minutes"]) | |
| return datetime.now() < expiry_time | |
| def filter_free_models(models_data: Dict) -> List[Dict]: | |
| """Filter models where both prompt and completion pricing are '0'""" | |
| if not models_data.get("data"): | |
| return [] | |
| free_models = [] | |
| for model in models_data["data"]: | |
| pricing = model.get("pricing", {}) | |
| prompt_price = pricing.get("prompt", "") | |
| completion_price = pricing.get("completion", "") | |
| # Check if both prompt and completion are "0" (free) | |
| if prompt_price == "0" and completion_price == "0": | |
| free_models.append(model) | |
| logger.debug(f"Found free model: {model.get('name', 'Unknown')}") | |
| logger.info(f"Found {len(free_models)} free models out of {len(models_data['data'])} total models") | |
| return free_models | |
| async def get_free_models_data(category: Optional[str] = None) -> List[Dict]: | |
| """Get free models data with caching""" | |
| # Use cached data if valid | |
| if is_cache_valid() and not category: # Don't use cache for category-specific requests | |
| logger.info("Using cached data") | |
| return filter_free_models(cache["data"]) | |
| # Fetch fresh data | |
| async with OpenRouterClient() as client: | |
| try: | |
| models_data = await client.get_models(category=category) | |
| # Update cache only for general requests (no category) | |
| if not category: | |
| cache["data"] = models_data | |
| cache["timestamp"] = datetime.now() | |
| logger.info("Updated cache with fresh data") | |
| return filter_free_models(models_data) | |
| except Exception as e: | |
| # If we have cached data, use it as fallback | |
| if cache["data"] and not category: | |
| logger.warning(f"API request failed, using cached data: {str(e)}") | |
| return filter_free_models(cache["data"]) | |
| else: | |
| raise HTTPException( | |
| status_code=503, | |
| detail=f"Failed to fetch models and no cached data available: {str(e)}" | |
| ) | |
| async def root(): | |
| """Root endpoint with API information""" | |
| return { | |
| "message": "Free Models API - Filter free AI models from OpenRouter", | |
| "version": "1.0.0", | |
| "endpoints": { | |
| "free_models": "/api/free-models", | |
| "free_models_by_category": "/api/free-models?category=programming", | |
| "category_route": "/api/category/{category}/free-models", | |
| "free_model_names": "/api/free-models/names", | |
| "specific_model": "/api/free-models/{model_id}", | |
| "supported_categories": "/api/categories", | |
| "health": "/health" | |
| }, | |
| "description": "This API fetches models from OpenRouter and filters for free models (prompt=0, completion=0)" | |
| } | |
| async def get_supported_categories(): | |
| """Get list of supported categories""" | |
| return { | |
| "success": True, | |
| "message": "List of supported categories", | |
| "categories": SUPPORTED_CATEGORIES, | |
| "usage": "Use these categories with /api/category/{category}/free-models" | |
| } | |
| async def get_free_models_by_category( | |
| category: str = Path(..., description="Category to filter by (e.g., chat, coding, vision)") | |
| ): | |
| """Get free models filtered by specific category""" | |
| # Validate category | |
| if category.lower() not in [cat.lower() for cat in SUPPORTED_CATEGORIES]: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Unsupported category '{category}'. Supported categories: {', '.join(SUPPORTED_CATEGORIES)}" | |
| ) | |
| try: | |
| free_models = await get_free_models_data(category=category.lower()) | |
| return FreeModelsResponse( | |
| success=True, | |
| message=f"Successfully retrieved {len(free_models)} free models in category '{category}'", | |
| count=len(free_models), | |
| models=free_models, | |
| filtered_at=datetime.now().isoformat() | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error in get_free_models_by_category: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_free_models( | |
| category: Optional[str] = Query(None, description="Filter by category (e.g., 'programming', 'chat')") | |
| ): | |
| """Get all free AI models from OpenRouter""" | |
| try: | |
| free_models = await get_free_models_data(category=category) | |
| return FreeModelsResponse( | |
| success=True, | |
| message=f"Successfully retrieved {len(free_models)} free models" + (f" in category '{category}'" if category else ""), | |
| count=len(free_models), | |
| models=free_models, | |
| filtered_at=datetime.now().isoformat() | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error in get_free_models: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_free_model_names( | |
| name: bool = False, | |
| category: Optional[str] = Query(None, description="Filter by category") | |
| ): | |
| """Get just the names and IDs of free models""" | |
| try: | |
| free_models = await get_free_models_data(category=category) | |
| if name: | |
| return { | |
| "success": True, | |
| "message": f"Retrieved {len(free_models)} free model names", | |
| "count": len(free_models), | |
| "models": [{"id": model["id"]} for model in free_models], | |
| "filtered_at": datetime.now().isoformat() | |
| } | |
| model_names = [ | |
| { | |
| "id": model["id"], | |
| "name": model["name"], | |
| "input_modalities": model.get("architecture", {}).get("input_modalities"), | |
| "output_modalities": model.get("architecture", {}).get("output_modalities"), | |
| "per_request_limits": model.get("per_request_limits", {}), | |
| "description": model.get("description", "")[:100] + "..." if len(model.get("description", "")) > 100 else model.get("description", "") | |
| } | |
| for model in free_models | |
| ] | |
| return { | |
| "success": True, | |
| "message": f"Retrieved {len(model_names)} free model names", | |
| "count": len(model_names), | |
| "models": model_names, | |
| "filtered_at": datetime.now().isoformat() | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error in get_free_model_names: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_free_model_by_id(model_id: str): | |
| """Get specific free model by ID""" | |
| try: | |
| free_models = await get_free_models_data() | |
| for model in free_models: | |
| if model["id"] == model_id: | |
| return { | |
| "success": True, | |
| "message": f"Found model {model_id}", | |
| "model": model | |
| } | |
| raise HTTPException( | |
| status_code=404, | |
| detail=f"Free model with ID '{model_id}' not found" | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Error in get_free_model_by_id: {str(e)}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def health_check(): | |
| """Health check endpoint""" | |
| try: | |
| # Test a simple API call to check connectivity | |
| async with OpenRouterClient() as client: | |
| await client.get_models() | |
| return { | |
| "status": "healthy", | |
| "timestamp": datetime.now().isoformat(), | |
| "cache_status": "valid" if is_cache_valid() else "expired", | |
| "openrouter_api": "accessible" | |
| } | |
| except Exception as e: | |
| return JSONResponse( | |
| status_code=503, | |
| content={ | |
| "status": "unhealthy", | |
| "timestamp": datetime.now().isoformat(), | |
| "error": str(e), | |
| "cache_status": "valid" if is_cache_valid() else "expired", | |
| "openrouter_api": "inaccessible" | |
| } | |
| ) | |
| async def ping(): | |
| """Ping endpoint to check API status""" | |
| return {"status": "ok"} |