|
|
|
|
|
"""
|
|
|
Dynamic Model API - REST endpoints for dynamic model loading
|
|
|
API برای بارگذاری هوشمند مدلها
|
|
|
"""
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Body
|
|
|
from pydantic import BaseModel, Field
|
|
|
from typing import Dict, Any, Optional, List
|
|
|
from datetime import datetime
|
|
|
|
|
|
from backend.services.dynamic_model_loader import dynamic_loader
|
|
|
|
|
|
router = APIRouter(prefix="/api/dynamic-models", tags=["Dynamic Models"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModelConfig(BaseModel):
|
|
|
"""تنظیمات مدل جدید"""
|
|
|
model_id: str = Field(..., description="Unique identifier for the model")
|
|
|
model_name: str = Field(..., description="Display name")
|
|
|
base_url: str = Field(..., description="Base URL of the API")
|
|
|
api_key: Optional[str] = Field(None, description="API key (if required)")
|
|
|
api_type: Optional[str] = Field(None, description="API type (auto-detected if not provided)")
|
|
|
endpoints: Optional[Dict[str, Any]] = Field(None, description="Custom endpoints (auto-discovered if not provided)")
|
|
|
custom_config: Optional[Dict[str, Any]] = Field(None, description="Additional configuration")
|
|
|
|
|
|
|
|
|
class PasteConfig(BaseModel):
|
|
|
"""
|
|
|
کپی/پیست تنظیمات از منابع مختلف
|
|
|
Supports multiple formats
|
|
|
"""
|
|
|
config_text: str = Field(..., description="Pasted configuration (JSON, YAML, or key-value pairs)")
|
|
|
auto_detect: bool = Field(True, description="Auto-detect format and API type")
|
|
|
|
|
|
|
|
|
class ModelUsageRequest(BaseModel):
|
|
|
"""درخواست استفاده از مدل"""
|
|
|
endpoint: str = Field(..., description="Endpoint to call (e.g., '', '/predict', '/generate')")
|
|
|
payload: Dict[str, Any] = Field(..., description="Request payload")
|
|
|
|
|
|
|
|
|
class DetectionRequest(BaseModel):
|
|
|
"""درخواست تشخیص نوع API"""
|
|
|
config: Dict[str, Any] = Field(..., description="Configuration to analyze")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/register")
|
|
|
async def register_model(config: ModelConfig):
|
|
|
"""
|
|
|
ثبت مدل جدید
|
|
|
|
|
|
**Usage**:
|
|
|
```json
|
|
|
{
|
|
|
"model_id": "my-custom-model",
|
|
|
"model_name": "My Custom Model",
|
|
|
"base_url": "https://api.example.com/models/my-model",
|
|
|
"api_key": "sk-xxxxx",
|
|
|
"api_type": "huggingface"
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Auto-Detection**:
|
|
|
- If `api_type` is not provided, it will be auto-detected
|
|
|
- If `endpoints` are not provided, they will be auto-discovered
|
|
|
"""
|
|
|
try:
|
|
|
result = await dynamic_loader.register_model(config.dict())
|
|
|
|
|
|
if not result['success']:
|
|
|
raise HTTPException(status_code=400, detail=result.get('error', 'Registration failed'))
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"message": "Model registered successfully",
|
|
|
"data": result
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Registration failed: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.post("/paste-config")
|
|
|
async def paste_configuration(paste: PasteConfig):
|
|
|
"""
|
|
|
کپی/پیست تنظیمات از هر منبعی
|
|
|
|
|
|
**Supported Formats**:
|
|
|
- JSON
|
|
|
- YAML
|
|
|
- Key-value pairs
|
|
|
- HuggingFace model cards
|
|
|
- OpenAI config
|
|
|
- cURL commands
|
|
|
|
|
|
**Example**:
|
|
|
```
|
|
|
{
|
|
|
"config_text": "{\\"model_id\\": \\"gpt-4\\", \\"base_url\\": \\"https://api.openai.com\\", ...}",
|
|
|
"auto_detect": true
|
|
|
}
|
|
|
```
|
|
|
"""
|
|
|
try:
|
|
|
import json
|
|
|
import yaml
|
|
|
|
|
|
config_text = paste.config_text.strip()
|
|
|
parsed_config = None
|
|
|
|
|
|
|
|
|
try:
|
|
|
parsed_config = json.loads(config_text)
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
|
|
|
if not parsed_config:
|
|
|
try:
|
|
|
parsed_config = yaml.safe_load(config_text)
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
|
|
|
if not parsed_config:
|
|
|
parsed_config = {}
|
|
|
for line in config_text.split('\n'):
|
|
|
if ':' in line or '=' in line:
|
|
|
separator = ':' if ':' in line else '='
|
|
|
parts = line.split(separator, 1)
|
|
|
if len(parts) == 2:
|
|
|
key = parts[0].strip().lower().replace(' ', '_')
|
|
|
value = parts[1].strip()
|
|
|
parsed_config[key] = value
|
|
|
|
|
|
if not parsed_config or not isinstance(parsed_config, dict):
|
|
|
raise HTTPException(
|
|
|
status_code=400,
|
|
|
detail="Could not parse configuration. Please provide valid JSON, YAML, or key-value pairs."
|
|
|
)
|
|
|
|
|
|
|
|
|
if 'model_id' not in parsed_config:
|
|
|
parsed_config['model_id'] = f"pasted-model-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
|
|
|
|
if 'model_name' not in parsed_config:
|
|
|
parsed_config['model_name'] = parsed_config['model_id']
|
|
|
|
|
|
if 'base_url' not in parsed_config:
|
|
|
raise HTTPException(
|
|
|
status_code=400,
|
|
|
detail="'base_url' is required in configuration"
|
|
|
)
|
|
|
|
|
|
|
|
|
if paste.auto_detect and 'api_type' not in parsed_config:
|
|
|
parsed_config['api_type'] = await dynamic_loader.detect_api_type(parsed_config)
|
|
|
|
|
|
|
|
|
result = await dynamic_loader.register_model(parsed_config)
|
|
|
|
|
|
if not result['success']:
|
|
|
raise HTTPException(status_code=400, detail=result.get('error', 'Registration failed'))
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"message": "Model registered from pasted configuration",
|
|
|
"parsed_config": parsed_config,
|
|
|
"data": result
|
|
|
}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to process pasted config: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.post("/detect-api-type")
|
|
|
async def detect_api_type(request: DetectionRequest):
|
|
|
"""
|
|
|
تشخیص خودکار نوع API
|
|
|
|
|
|
**Example**:
|
|
|
```json
|
|
|
{
|
|
|
"config": {
|
|
|
"base_url": "/static-proxy?url=https%3A%2F%2Fapi-inference.huggingface.co%2Fmodels%2Fbert-base%26quot%3B%2C
|
|
|
"api_key": "hf_xxxxx"
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Returns**: Detected API type (huggingface, openai, rest, graphql, etc.)
|
|
|
"""
|
|
|
try:
|
|
|
api_type = await dynamic_loader.detect_api_type(request.config)
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"api_type": api_type,
|
|
|
"config": request.config
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Detection failed: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.post("/test-connection")
|
|
|
async def test_connection(config: ModelConfig):
|
|
|
"""
|
|
|
تست اتصال به مدل بدون ثبت
|
|
|
|
|
|
**Usage**: Test before registering
|
|
|
"""
|
|
|
try:
|
|
|
result = await dynamic_loader.test_model_connection(config.dict())
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"test_result": result
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Test failed: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.get("/models")
|
|
|
async def get_all_models():
|
|
|
"""
|
|
|
دریافت لیست همه مدلهای ثبت شده
|
|
|
|
|
|
**Returns**: List of all registered dynamic models
|
|
|
"""
|
|
|
try:
|
|
|
models = dynamic_loader.get_all_models()
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"total": len(models),
|
|
|
"models": models
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get models: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.get("/models/{model_id}")
|
|
|
async def get_model(model_id: str):
|
|
|
"""
|
|
|
دریافت اطلاعات یک مدل خاص
|
|
|
"""
|
|
|
try:
|
|
|
model = dynamic_loader.get_model(model_id)
|
|
|
|
|
|
if not model:
|
|
|
raise HTTPException(status_code=404, detail=f"Model not found: {model_id}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"model": model
|
|
|
}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get model: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.post("/models/{model_id}/use")
|
|
|
async def use_model(model_id: str, usage: ModelUsageRequest):
|
|
|
"""
|
|
|
استفاده از یک مدل ثبت شده
|
|
|
|
|
|
**Example**:
|
|
|
```json
|
|
|
{
|
|
|
"endpoint": "",
|
|
|
"payload": {
|
|
|
"inputs": "Bitcoin is bullish!"
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
"""
|
|
|
try:
|
|
|
result = await dynamic_loader.use_model(
|
|
|
model_id,
|
|
|
usage.endpoint,
|
|
|
usage.payload
|
|
|
)
|
|
|
|
|
|
if not result['success']:
|
|
|
raise HTTPException(status_code=400, detail=result.get('error', 'Model usage failed'))
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"data": result
|
|
|
}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to use model: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.delete("/models/{model_id}")
|
|
|
async def delete_model(model_id: str):
|
|
|
"""
|
|
|
حذف یک مدل
|
|
|
"""
|
|
|
try:
|
|
|
success = dynamic_loader.delete_model(model_id)
|
|
|
|
|
|
if not success:
|
|
|
raise HTTPException(status_code=404, detail=f"Model not found: {model_id}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"message": f"Model {model_id} deleted successfully"
|
|
|
}
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete model: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.post("/auto-configure")
|
|
|
async def auto_configure_from_url(url: str = Body(..., embed=True)):
|
|
|
"""
|
|
|
تنظیم خودکار کامل از URL
|
|
|
|
|
|
**Usage**: Just provide a URL, everything else is auto-detected
|
|
|
|
|
|
**Example**:
|
|
|
```json
|
|
|
{
|
|
|
"url": "/static-proxy?url=https%3A%2F%2Fapi-inference.huggingface.co%2Fmodels%2Fbert-base-uncased%26quot%3B
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Process**:
|
|
|
1. Auto-detect API type from URL
|
|
|
2. Auto-discover endpoints
|
|
|
3. Test connection
|
|
|
4. Register if successful
|
|
|
"""
|
|
|
try:
|
|
|
|
|
|
config = {
|
|
|
'model_id': url.split('/')[-1] or f'auto-{datetime.now().strftime("%Y%m%d%H%M%S")}',
|
|
|
'model_name': url.split('/')[-1] or 'Auto-configured Model',
|
|
|
'base_url': url
|
|
|
}
|
|
|
|
|
|
|
|
|
api_type = await dynamic_loader.detect_api_type(config)
|
|
|
config['api_type'] = api_type
|
|
|
|
|
|
|
|
|
discovered = await dynamic_loader.auto_discover_endpoints(url)
|
|
|
config['endpoints'] = discovered
|
|
|
|
|
|
|
|
|
test_result = await dynamic_loader.test_model_connection(config)
|
|
|
|
|
|
if not test_result['success']:
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": "Connection test failed",
|
|
|
"test_result": test_result,
|
|
|
"config": config,
|
|
|
"message": "Model configuration created but connection failed. You can still register it manually."
|
|
|
}
|
|
|
|
|
|
|
|
|
result = await dynamic_loader.register_model(config)
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"message": "Model auto-configured and registered successfully",
|
|
|
"config": config,
|
|
|
"test_result": test_result,
|
|
|
"registration": result
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
raise HTTPException(status_code=500, detail=f"Auto-configuration failed: {str(e)}")
|
|
|
|
|
|
|
|
|
@router.get("/health")
|
|
|
async def health_check():
|
|
|
"""سلامت سیستم"""
|
|
|
return {
|
|
|
"status": "healthy",
|
|
|
"timestamp": datetime.now().isoformat()
|
|
|
}
|
|
|
|
|
|
|