- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2 - Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2 - Made more general-purpose (flexible targets, expanded tools) - Added nemotron-mini-agent.csv for fast local inference via Ollama - Added nemotron-agent.csv for advanced reasoning via Ollama - Added wiki page: Projects for NVIDIA NIMs and Nemotron
362 lines
12 KiB
Python
362 lines
12 KiB
Python
"""
|
|
Tenant Model Management API for GT 2.0 Admin Control Panel
|
|
|
|
Provides endpoints for managing which models are available to which tenants,
|
|
with tenant-specific permissions and rate limits.
|
|
"""
|
|
|
|
from typing import Dict, Any, List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from pydantic import BaseModel, Field
|
|
import logging
|
|
|
|
from app.core.database import get_db
|
|
from app.services.model_management_service import get_model_management_service
|
|
from app.models.tenant_model_config import TenantModelConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/tenants", tags=["Tenant Model Management"])
|
|
|
|
|
|
# Request/Response Models
|
|
class TenantModelAssignRequest(BaseModel):
|
|
model_id: str = Field(..., description="Model ID to assign")
|
|
rate_limits: Optional[Dict[str, Any]] = Field(None, description="Custom rate limits")
|
|
capabilities: Optional[Dict[str, Any]] = Field(None, description="Tenant-specific capabilities")
|
|
usage_constraints: Optional[Dict[str, Any]] = Field(None, description="Usage restrictions")
|
|
priority: int = Field(1, ge=1, le=10, description="Priority level (1-10)")
|
|
|
|
model_config = {"protected_namespaces": ()}
|
|
|
|
|
|
class TenantModelUpdateRequest(BaseModel):
|
|
is_enabled: Optional[bool] = Field(None, description="Enable/disable model for tenant")
|
|
rate_limits: Optional[Dict[str, Any]] = Field(None, description="Updated rate limits")
|
|
tenant_capabilities: Optional[Dict[str, Any]] = Field(None, description="Updated capabilities")
|
|
usage_constraints: Optional[Dict[str, Any]] = Field(None, description="Updated usage restrictions")
|
|
priority: Optional[int] = Field(None, ge=1, le=10, description="Updated priority level")
|
|
|
|
|
|
class ModelAccessCheckRequest(BaseModel):
|
|
user_capabilities: Optional[List[str]] = Field(None, description="User capabilities")
|
|
user_id: Optional[str] = Field(None, description="User identifier")
|
|
|
|
|
|
class TenantModelResponse(BaseModel):
|
|
id: int
|
|
tenant_id: int
|
|
model_id: str
|
|
is_enabled: bool
|
|
tenant_capabilities: Dict[str, Any]
|
|
rate_limits: Dict[str, Any]
|
|
usage_constraints: Dict[str, Any]
|
|
priority: int
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
|
|
class ModelWithTenantConfigResponse(BaseModel):
|
|
model_id: str
|
|
name: str
|
|
provider: str
|
|
model_type: str
|
|
endpoint: str
|
|
tenant_config: TenantModelResponse
|
|
|
|
|
|
@router.post("/{tenant_id}/models", response_model=TenantModelResponse)
|
|
async def assign_model_to_tenant(
|
|
tenant_id: int,
|
|
request: TenantModelAssignRequest,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Assign a model to a tenant with specific configuration"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
tenant_model_config = await service.assign_model_to_tenant(
|
|
tenant_id=tenant_id,
|
|
model_id=request.model_id,
|
|
rate_limits=request.rate_limits,
|
|
capabilities=request.capabilities,
|
|
usage_constraints=request.usage_constraints,
|
|
priority=request.priority
|
|
)
|
|
|
|
return TenantModelResponse(**tenant_model_config.to_dict())
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error assigning model to tenant: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{tenant_id}/models/{model_id:path}")
|
|
async def remove_model_from_tenant(
|
|
tenant_id: int,
|
|
model_id: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Remove model access from a tenant"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
success = await service.remove_model_from_tenant(tenant_id, model_id)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="Model assignment not found")
|
|
|
|
return {"message": f"Model {model_id} removed from tenant {tenant_id}"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error removing model from tenant: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.patch("/{tenant_id}/models/{model_id:path}", response_model=TenantModelResponse)
|
|
async def update_tenant_model_config(
|
|
tenant_id: int,
|
|
model_id: str,
|
|
request: TenantModelUpdateRequest,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Update tenant-specific model configuration"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
# Convert request to dict, excluding None values
|
|
updates = {k: v for k, v in request.dict().items() if v is not None}
|
|
|
|
tenant_model_config = await service.update_tenant_model_config(
|
|
tenant_id=tenant_id,
|
|
model_id=model_id,
|
|
updates=updates
|
|
)
|
|
|
|
if not tenant_model_config:
|
|
raise HTTPException(status_code=404, detail="Tenant model configuration not found")
|
|
|
|
return TenantModelResponse(**tenant_model_config.to_dict())
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error updating tenant model config: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/{tenant_id}/models", response_model=List[ModelWithTenantConfigResponse])
|
|
async def get_tenant_models(
|
|
tenant_id: int,
|
|
enabled_only: bool = Query(False, description="Only return enabled models"),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get all models available to a tenant"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
models = await service.get_tenant_models(
|
|
tenant_id=tenant_id,
|
|
enabled_only=enabled_only
|
|
)
|
|
|
|
# Format response
|
|
response_models = []
|
|
for model in models:
|
|
tenant_config = model.pop("tenant_config")
|
|
response_models.append({
|
|
**model,
|
|
"tenant_config": TenantModelResponse(**tenant_config)
|
|
})
|
|
|
|
return response_models
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting tenant models: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/{tenant_id}/models/{model_id}/check-access")
|
|
async def check_tenant_model_access(
|
|
tenant_id: int,
|
|
model_id: str,
|
|
request: ModelAccessCheckRequest,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Check if a tenant/user can access a specific model"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
access_info = await service.check_tenant_model_access(
|
|
tenant_id=tenant_id,
|
|
model_id=model_id,
|
|
user_capabilities=request.user_capabilities,
|
|
user_id=request.user_id
|
|
)
|
|
|
|
return access_info
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking tenant model access: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/{tenant_id}/models/stats")
|
|
async def get_tenant_model_stats(
|
|
tenant_id: int,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get statistics about models for a tenant"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
stats = await service.get_tenant_model_stats(tenant_id)
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting tenant model stats: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# Additional endpoints for model-centric views
|
|
@router.get("/models/{model_id:path}/tenants")
|
|
async def get_model_tenants(
|
|
model_id: str,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get all tenants that have access to a model"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
tenants = await service.get_model_tenants(model_id)
|
|
|
|
return {
|
|
"model_id": model_id,
|
|
"tenants": tenants,
|
|
"total_tenants": len(tenants)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting model tenants: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# Global tenant model configuration endpoints
|
|
@router.get("/all")
|
|
async def get_all_tenant_model_configs(
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get all tenant model configurations with joined tenant and model data"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
# This would need to be implemented in the service
|
|
configs = await service.get_all_tenant_model_configs()
|
|
|
|
return configs
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting all tenant model configs: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# Bulk operations
|
|
@router.post("/{tenant_id}/models/bulk-assign")
|
|
async def bulk_assign_models_to_tenant(
|
|
tenant_id: int,
|
|
model_ids: List[str],
|
|
default_config: Optional[TenantModelAssignRequest] = None,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Assign multiple models to a tenant with the same configuration"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
results = []
|
|
errors = []
|
|
|
|
for model_id in model_ids:
|
|
try:
|
|
config = default_config if default_config else TenantModelAssignRequest(model_id=model_id)
|
|
|
|
tenant_model_config = await service.assign_model_to_tenant(
|
|
tenant_id=tenant_id,
|
|
model_id=model_id,
|
|
rate_limits=config.rate_limits,
|
|
capabilities=config.capabilities,
|
|
usage_constraints=config.usage_constraints,
|
|
priority=config.priority
|
|
)
|
|
|
|
results.append({
|
|
"model_id": model_id,
|
|
"status": "success",
|
|
"config": tenant_model_config.to_dict()
|
|
})
|
|
|
|
except Exception as e:
|
|
errors.append({
|
|
"model_id": model_id,
|
|
"status": "error",
|
|
"error": str(e)
|
|
})
|
|
|
|
return {
|
|
"tenant_id": tenant_id,
|
|
"total_requested": len(model_ids),
|
|
"successful": len(results),
|
|
"failed": len(errors),
|
|
"results": results,
|
|
"errors": errors
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error bulk assigning models: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.delete("/{tenant_id}/models/bulk-remove")
|
|
async def bulk_remove_models_from_tenant(
|
|
tenant_id: int,
|
|
model_ids: List[str],
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Remove multiple models from a tenant"""
|
|
try:
|
|
service = get_model_management_service(db)
|
|
|
|
results = []
|
|
|
|
for model_id in model_ids:
|
|
try:
|
|
success = await service.remove_model_from_tenant(tenant_id, model_id)
|
|
results.append({
|
|
"model_id": model_id,
|
|
"status": "success" if success else "not_found",
|
|
"removed": success
|
|
})
|
|
|
|
except Exception as e:
|
|
results.append({
|
|
"model_id": model_id,
|
|
"status": "error",
|
|
"error": str(e)
|
|
})
|
|
|
|
successful = sum(1 for r in results if r["status"] == "success")
|
|
|
|
return {
|
|
"tenant_id": tenant_id,
|
|
"total_requested": len(model_ids),
|
|
"successful": successful,
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error bulk removing models: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e)) |