feat: add marketplace metrics, privacy features, and service registry endpoints
- Add Prometheus metrics for marketplace API throughput and error rates with new dashboard panels - Implement confidential transaction models with encryption support and access control - Add key management system with registration, rotation, and audit logging - Create services and registry routers for service discovery and management - Integrate ZK proof generation for privacy-preserving receipts - Add metrics instru
This commit is contained in:
@ -7,7 +7,7 @@ from fastapi import FastAPI
|
||||
from ..database import close_engine, create_engine
|
||||
from ..redis_cache import close_redis, create_redis
|
||||
from ..settings import settings
|
||||
from .routers import health_router, match_router, metrics_router
|
||||
from .routers import health_router, match_router, metrics_router, services, ui, validation
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@ -25,6 +25,9 @@ app = FastAPI(**settings.asgi_kwargs(), lifespan=lifespan)
|
||||
app.include_router(match_router, prefix="/v1")
|
||||
app.include_router(health_router)
|
||||
app.include_router(metrics_router)
|
||||
app.include_router(services, prefix="/v1")
|
||||
app.include_router(ui)
|
||||
app.include_router(validation, prefix="/v1")
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
|
||||
302
apps/pool-hub/src/poolhub/app/routers/services.py
Normal file
302
apps/pool-hub/src/poolhub/app/routers/services.py
Normal file
@ -0,0 +1,302 @@
|
||||
"""
|
||||
Service configuration router for pool hub
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..deps import get_db, get_miner_id
|
||||
from ..models import Miner, ServiceConfig, ServiceType
|
||||
from ..schemas import ServiceConfigCreate, ServiceConfigUpdate, ServiceConfigResponse
|
||||
|
||||
router = APIRouter(prefix="/services", tags=["services"])
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ServiceConfigResponse])
|
||||
async def list_service_configs(
|
||||
db: Session = Depends(get_db),
|
||||
miner_id: str = Depends(get_miner_id)
|
||||
) -> List[ServiceConfigResponse]:
|
||||
"""List all service configurations for the miner"""
|
||||
stmt = select(ServiceConfig).where(ServiceConfig.miner_id == miner_id)
|
||||
configs = db.execute(stmt).scalars().all()
|
||||
|
||||
return [ServiceConfigResponse.from_orm(config) for config in configs]
|
||||
|
||||
|
||||
@router.get("/{service_type}", response_model=ServiceConfigResponse)
|
||||
async def get_service_config(
|
||||
service_type: str,
|
||||
db: Session = Depends(get_db),
|
||||
miner_id: str = Depends(get_miner_id)
|
||||
) -> ServiceConfigResponse:
|
||||
"""Get configuration for a specific service"""
|
||||
stmt = select(ServiceConfig).where(
|
||||
ServiceConfig.miner_id == miner_id,
|
||||
ServiceConfig.service_type == service_type
|
||||
)
|
||||
config = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if not config:
|
||||
# Return default config
|
||||
return ServiceConfigResponse(
|
||||
service_type=service_type,
|
||||
enabled=False,
|
||||
config={},
|
||||
pricing={},
|
||||
capabilities=[],
|
||||
max_concurrent=1
|
||||
)
|
||||
|
||||
return ServiceConfigResponse.from_orm(config)
|
||||
|
||||
|
||||
@router.post("/{service_type}", response_model=ServiceConfigResponse)
|
||||
async def create_or_update_service_config(
|
||||
service_type: str,
|
||||
config_data: ServiceConfigCreate,
|
||||
db: Session = Depends(get_db),
|
||||
miner_id: str = Depends(get_miner_id)
|
||||
) -> ServiceConfigResponse:
|
||||
"""Create or update service configuration"""
|
||||
# Validate service type
|
||||
if service_type not in [s.value for s in ServiceType]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid service type: {service_type}"
|
||||
)
|
||||
|
||||
# Check if config exists
|
||||
stmt = select(ServiceConfig).where(
|
||||
ServiceConfig.miner_id == miner_id,
|
||||
ServiceConfig.service_type == service_type
|
||||
)
|
||||
existing = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
# Update existing
|
||||
existing.enabled = config_data.enabled
|
||||
existing.config = config_data.config
|
||||
existing.pricing = config_data.pricing
|
||||
existing.capabilities = config_data.capabilities
|
||||
existing.max_concurrent = config_data.max_concurrent
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
config = existing
|
||||
else:
|
||||
# Create new
|
||||
config = ServiceConfig(
|
||||
miner_id=miner_id,
|
||||
service_type=service_type,
|
||||
enabled=config_data.enabled,
|
||||
config=config_data.config,
|
||||
pricing=config_data.pricing,
|
||||
capabilities=config_data.capabilities,
|
||||
max_concurrent=config_data.max_concurrent
|
||||
)
|
||||
db.add(config)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
|
||||
return ServiceConfigResponse.from_orm(config)
|
||||
|
||||
|
||||
@router.patch("/{service_type}", response_model=ServiceConfigResponse)
|
||||
async def patch_service_config(
|
||||
service_type: str,
|
||||
config_data: ServiceConfigUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
miner_id: str = Depends(get_miner_id)
|
||||
) -> ServiceConfigResponse:
|
||||
"""Partially update service configuration"""
|
||||
stmt = select(ServiceConfig).where(
|
||||
ServiceConfig.miner_id == miner_id,
|
||||
ServiceConfig.service_type == service_type
|
||||
)
|
||||
config = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Service configuration not found"
|
||||
)
|
||||
|
||||
# Update only provided fields
|
||||
if config_data.enabled is not None:
|
||||
config.enabled = config_data.enabled
|
||||
if config_data.config is not None:
|
||||
config.config = config_data.config
|
||||
if config_data.pricing is not None:
|
||||
config.pricing = config_data.pricing
|
||||
if config_data.capabilities is not None:
|
||||
config.capabilities = config_data.capabilities
|
||||
if config_data.max_concurrent is not None:
|
||||
config.max_concurrent = config_data.max_concurrent
|
||||
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
|
||||
return ServiceConfigResponse.from_orm(config)
|
||||
|
||||
|
||||
@router.delete("/{service_type}")
|
||||
async def delete_service_config(
|
||||
service_type: str,
|
||||
db: Session = Depends(get_db),
|
||||
miner_id: str = Depends(get_miner_id)
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete service configuration"""
|
||||
stmt = select(ServiceConfig).where(
|
||||
ServiceConfig.miner_id == miner_id,
|
||||
ServiceConfig.service_type == service_type
|
||||
)
|
||||
config = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Service configuration not found"
|
||||
)
|
||||
|
||||
db.delete(config)
|
||||
db.commit()
|
||||
|
||||
return {"message": f"Service configuration for {service_type} deleted"}
|
||||
|
||||
|
||||
@router.get("/templates/{service_type}")
|
||||
async def get_service_template(service_type: str) -> Dict[str, Any]:
|
||||
"""Get default configuration template for a service"""
|
||||
templates = {
|
||||
"whisper": {
|
||||
"config": {
|
||||
"models": ["tiny", "base", "small", "medium", "large"],
|
||||
"default_model": "base",
|
||||
"max_file_size_mb": 500,
|
||||
"supported_formats": ["mp3", "wav", "m4a", "flac"]
|
||||
},
|
||||
"pricing": {
|
||||
"per_minute": 0.001,
|
||||
"min_charge": 0.01
|
||||
},
|
||||
"capabilities": ["transcribe", "translate"],
|
||||
"max_concurrent": 2
|
||||
},
|
||||
"stable_diffusion": {
|
||||
"config": {
|
||||
"models": ["stable-diffusion-1.5", "stable-diffusion-2.1", "sdxl"],
|
||||
"default_model": "stable-diffusion-1.5",
|
||||
"max_resolution": "1024x1024",
|
||||
"max_images_per_request": 4
|
||||
},
|
||||
"pricing": {
|
||||
"per_image": 0.01,
|
||||
"per_step": 0.001
|
||||
},
|
||||
"capabilities": ["txt2img", "img2img"],
|
||||
"max_concurrent": 1
|
||||
},
|
||||
"llm_inference": {
|
||||
"config": {
|
||||
"models": ["llama-7b", "llama-13b", "mistral-7b", "mixtral-8x7b"],
|
||||
"default_model": "llama-7b",
|
||||
"max_tokens": 4096,
|
||||
"context_length": 4096
|
||||
},
|
||||
"pricing": {
|
||||
"per_1k_tokens": 0.001,
|
||||
"min_charge": 0.01
|
||||
},
|
||||
"capabilities": ["generate", "stream"],
|
||||
"max_concurrent": 2
|
||||
},
|
||||
"ffmpeg": {
|
||||
"config": {
|
||||
"supported_codecs": ["h264", "h265", "vp9"],
|
||||
"max_resolution": "4K",
|
||||
"max_file_size_gb": 10,
|
||||
"gpu_acceleration": True
|
||||
},
|
||||
"pricing": {
|
||||
"per_minute": 0.005,
|
||||
"per_gb": 0.01
|
||||
},
|
||||
"capabilities": ["transcode", "resize", "compress"],
|
||||
"max_concurrent": 1
|
||||
},
|
||||
"blender": {
|
||||
"config": {
|
||||
"engines": ["cycles", "eevee"],
|
||||
"default_engine": "cycles",
|
||||
"max_samples": 4096,
|
||||
"max_resolution": "4K"
|
||||
},
|
||||
"pricing": {
|
||||
"per_frame": 0.01,
|
||||
"per_hour": 0.5
|
||||
},
|
||||
"capabilities": ["render", "animation"],
|
||||
"max_concurrent": 1
|
||||
}
|
||||
}
|
||||
|
||||
if service_type not in templates:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Unknown service type: {service_type}"
|
||||
)
|
||||
|
||||
return templates[service_type]
|
||||
|
||||
|
||||
@router.post("/validate/{service_type}")
|
||||
async def validate_service_config(
|
||||
service_type: str,
|
||||
config_data: Dict[str, Any],
|
||||
db: Session = Depends(get_db),
|
||||
miner_id: str = Depends(get_miner_id)
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate service configuration against miner capabilities"""
|
||||
# Get miner info
|
||||
stmt = select(Miner).where(Miner.miner_id == miner_id)
|
||||
miner = db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if not miner:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Miner not found"
|
||||
)
|
||||
|
||||
# Validate based on service type
|
||||
validation_result = {
|
||||
"valid": True,
|
||||
"warnings": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
if service_type == "stable_diffusion":
|
||||
# Check VRAM requirements
|
||||
max_resolution = config_data.get("config", {}).get("max_resolution", "1024x1024")
|
||||
if "4K" in max_resolution and miner.gpu_vram_gb < 16:
|
||||
validation_result["warnings"].append("4K resolution requires at least 16GB VRAM")
|
||||
|
||||
if miner.gpu_vram_gb < 8:
|
||||
validation_result["errors"].append("Stable Diffusion requires at least 8GB VRAM")
|
||||
validation_result["valid"] = False
|
||||
|
||||
elif service_type == "llm_inference":
|
||||
# Check model size vs VRAM
|
||||
models = config_data.get("config", {}).get("models", [])
|
||||
for model in models:
|
||||
if "70b" in model.lower() and miner.gpu_vram_gb < 64:
|
||||
validation_result["warnings"].append(f"{model} requires 64GB VRAM")
|
||||
|
||||
elif service_type == "blender":
|
||||
# Check if GPU is supported
|
||||
engine = config_data.get("config", {}).get("default_engine", "cycles")
|
||||
if engine == "cycles" and "nvidia" not in miner.tags.get("gpu", "").lower():
|
||||
validation_result["warnings"].append("Cycles engine works best with NVIDIA GPUs")
|
||||
|
||||
return validation_result
|
||||
20
apps/pool-hub/src/poolhub/app/routers/ui.py
Normal file
20
apps/pool-hub/src/poolhub/app/routers/ui.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""
|
||||
UI router for serving static HTML pages
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import os
|
||||
|
||||
router = APIRouter(tags=["ui"])
|
||||
|
||||
# Get templates directory
|
||||
templates_dir = os.path.join(os.path.dirname(__file__), "..", "templates")
|
||||
templates = Jinja2Templates(directory=templates_dir)
|
||||
|
||||
|
||||
@router.get("/services", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def services_ui(request: Request):
|
||||
"""Serve the service configuration UI"""
|
||||
return templates.TemplateResponse("services.html", {"request": request})
|
||||
181
apps/pool-hub/src/poolhub/app/routers/validation.py
Normal file
181
apps/pool-hub/src/poolhub/app/routers/validation.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""
|
||||
Validation router for service configuration validation
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..deps import get_miner_from_token
|
||||
from ..models import Miner
|
||||
from ..services.validation import HardwareValidator, ValidationResult
|
||||
|
||||
router = APIRouter(tags=["validation"])
|
||||
validator = HardwareValidator()
|
||||
|
||||
|
||||
@router.post("/validation/service/{service_id}")
|
||||
async def validate_service(
|
||||
service_id: str,
|
||||
config: Dict[str, Any],
|
||||
miner: Miner = Depends(get_miner_from_token)
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate if miner can run a specific service with given configuration"""
|
||||
|
||||
result = await validator.validate_service_for_miner(miner, service_id, config)
|
||||
|
||||
return {
|
||||
"valid": result.valid,
|
||||
"errors": result.errors,
|
||||
"warnings": result.warnings,
|
||||
"score": result.score,
|
||||
"missing_requirements": result.missing_requirements,
|
||||
"performance_impact": result.performance_impact
|
||||
}
|
||||
|
||||
|
||||
@router.get("/validation/compatible-services")
|
||||
async def get_compatible_services(
|
||||
miner: Miner = Depends(get_miner_from_token)
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get list of services compatible with miner hardware, sorted by compatibility score"""
|
||||
|
||||
compatible = await validator.get_compatible_services(miner)
|
||||
|
||||
return [
|
||||
{
|
||||
"service_id": service_id,
|
||||
"compatibility_score": score,
|
||||
"grade": _get_grade_from_score(score)
|
||||
}
|
||||
for service_id, score in compatible
|
||||
]
|
||||
|
||||
|
||||
@router.post("/validation/batch")
|
||||
async def validate_multiple_services(
|
||||
validations: List[Dict[str, Any]],
|
||||
miner: Miner = Depends(get_miner_from_token)
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Validate multiple service configurations in batch"""
|
||||
|
||||
results = []
|
||||
|
||||
for validation in validations:
|
||||
service_id = validation.get("service_id")
|
||||
config = validation.get("config", {})
|
||||
|
||||
if not service_id:
|
||||
results.append({
|
||||
"service_id": service_id,
|
||||
"valid": False,
|
||||
"errors": ["Missing service_id"]
|
||||
})
|
||||
continue
|
||||
|
||||
result = await validator.validate_service_for_miner(miner, service_id, config)
|
||||
|
||||
results.append({
|
||||
"service_id": service_id,
|
||||
"valid": result.valid,
|
||||
"errors": result.errors,
|
||||
"warnings": result.warnings,
|
||||
"score": result.score,
|
||||
"performance_impact": result.performance_impact
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/validation/hardware-profile")
|
||||
async def get_hardware_profile(
|
||||
miner: Miner = Depends(get_miner_from_token)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get miner's hardware profile with capabilities assessment"""
|
||||
|
||||
# Get compatible services to assess capabilities
|
||||
compatible = await validator.get_compatible_services(miner)
|
||||
|
||||
# Analyze hardware capabilities
|
||||
profile = {
|
||||
"miner_id": miner.id,
|
||||
"hardware": {
|
||||
"gpu": {
|
||||
"name": miner.gpu_name,
|
||||
"vram_gb": miner.gpu_vram_gb,
|
||||
"available": miner.gpu_name is not None
|
||||
},
|
||||
"cpu": {
|
||||
"cores": miner.cpu_cores
|
||||
},
|
||||
"ram": {
|
||||
"gb": miner.ram_gb
|
||||
},
|
||||
"capabilities": miner.capabilities,
|
||||
"tags": miner.tags
|
||||
},
|
||||
"assessment": {
|
||||
"total_services": len(compatible),
|
||||
"highly_compatible": len([s for s in compatible if s[1] >= 80]),
|
||||
"moderately_compatible": len([s for s in compatible if 50 <= s[1] < 80]),
|
||||
"barely_compatible": len([s for s in compatible if s[1] < 50]),
|
||||
"best_categories": _get_best_categories(compatible)
|
||||
},
|
||||
"recommendations": _generate_recommendations(miner, compatible)
|
||||
}
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
def _get_grade_from_score(score: int) -> str:
|
||||
"""Convert compatibility score to letter grade"""
|
||||
if score >= 90:
|
||||
return "A+"
|
||||
elif score >= 80:
|
||||
return "A"
|
||||
elif score >= 70:
|
||||
return "B"
|
||||
elif score >= 60:
|
||||
return "C"
|
||||
elif score >= 50:
|
||||
return "D"
|
||||
else:
|
||||
return "F"
|
||||
|
||||
|
||||
def _get_best_categories(compatible: List[tuple]) -> List[str]:
|
||||
"""Get the categories with highest compatibility"""
|
||||
# This would need category info from registry
|
||||
# For now, return placeholder
|
||||
return ["AI/ML", "Media Processing"]
|
||||
|
||||
|
||||
def _generate_recommendations(miner: Miner, compatible: List[tuple]) -> List[str]:
|
||||
"""Generate hardware upgrade recommendations"""
|
||||
recommendations = []
|
||||
|
||||
# Check VRAM
|
||||
if miner.gpu_vram_gb < 8:
|
||||
recommendations.append("Upgrade GPU to at least 8GB VRAM for better AI/ML performance")
|
||||
elif miner.gpu_vram_gb < 16:
|
||||
recommendations.append("Consider upgrading to 16GB+ VRAM for optimal performance")
|
||||
|
||||
# Check CPU
|
||||
if miner.cpu_cores < 8:
|
||||
recommendations.append("More CPU cores would improve parallel processing")
|
||||
|
||||
# Check RAM
|
||||
if miner.ram_gb < 16:
|
||||
recommendations.append("Upgrade to 16GB+ RAM for better multitasking")
|
||||
|
||||
# Check capabilities
|
||||
if "cuda" not in [c.lower() for c in miner.capabilities]:
|
||||
recommendations.append("CUDA support would enable more GPU services")
|
||||
|
||||
# Based on compatible services
|
||||
if len(compatible) < 10:
|
||||
recommendations.append("Hardware upgrade recommended to access more services")
|
||||
elif len(compatible) > 20:
|
||||
recommendations.append("Your hardware is well-suited for a wide range of services")
|
||||
|
||||
return recommendations
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -10,6 +11,7 @@ class MatchRequestPayload(BaseModel):
|
||||
requirements: Dict[str, Any] = Field(default_factory=dict)
|
||||
hints: Dict[str, Any] = Field(default_factory=dict)
|
||||
top_k: int = Field(default=1, ge=1, le=50)
|
||||
redis_error: Optional[str] = None
|
||||
|
||||
|
||||
class MatchCandidate(BaseModel):
|
||||
@ -38,3 +40,37 @@ class HealthResponse(BaseModel):
|
||||
|
||||
class MetricsResponse(BaseModel):
|
||||
detail: str = "Prometheus metrics output"
|
||||
|
||||
|
||||
# Service Configuration Schemas
|
||||
class ServiceConfigBase(BaseModel):
|
||||
"""Base service configuration"""
|
||||
enabled: bool = Field(False, description="Whether service is enabled")
|
||||
config: Dict[str, Any] = Field(default_factory=dict, description="Service-specific configuration")
|
||||
pricing: Dict[str, Any] = Field(default_factory=dict, description="Pricing configuration")
|
||||
capabilities: List[str] = Field(default_factory=list, description="Service capabilities")
|
||||
max_concurrent: int = Field(1, ge=1, le=10, description="Maximum concurrent jobs")
|
||||
|
||||
|
||||
class ServiceConfigCreate(ServiceConfigBase):
|
||||
"""Service configuration creation request"""
|
||||
pass
|
||||
|
||||
|
||||
class ServiceConfigUpdate(BaseModel):
|
||||
"""Service configuration update request"""
|
||||
enabled: Optional[bool] = Field(None, description="Whether service is enabled")
|
||||
config: Optional[Dict[str, Any]] = Field(None, description="Service-specific configuration")
|
||||
pricing: Optional[Dict[str, Any]] = Field(None, description="Pricing configuration")
|
||||
capabilities: Optional[List[str]] = Field(None, description="Service capabilities")
|
||||
max_concurrent: Optional[int] = Field(None, ge=1, le=10, description="Maximum concurrent jobs")
|
||||
|
||||
|
||||
class ServiceConfigResponse(ServiceConfigBase):
|
||||
"""Service configuration response"""
|
||||
service_type: str = Field(..., description="Service type")
|
||||
created_at: datetime = Field(..., description="Creation time")
|
||||
updated_at: datetime = Field(..., description="Last update time")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
990
apps/pool-hub/src/poolhub/app/templates/services.html
Normal file
990
apps/pool-hub/src/poolhub/app/templates/services.html
Normal file
@ -0,0 +1,990 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Service Configuration - AITBC Pool Hub</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#categoryFilter {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#categoryFilter:focus {
|
||||
outline: none;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 16px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #4caf50;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.price-input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.price-unit {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.capabilities-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.capability-tag {
|
||||
padding: 4px 12px;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e2e5;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 16px 24px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: none;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #4caf50;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<h1>Service Configuration</h1>
|
||||
<div class="header-controls">
|
||||
<select id="categoryFilter" onchange="filterByCategory()">
|
||||
<option value="">All Categories</option>
|
||||
<option value="ai_ml">AI/ML</option>
|
||||
<option value="media_processing">Media Processing</option>
|
||||
<option value="scientific_computing">Scientific Computing</option>
|
||||
<option value="data_analytics">Data Analytics</option>
|
||||
<option value="gaming_entertainment">Gaming & Entertainment</option>
|
||||
<option value="development_tools">Development Tools</option>
|
||||
</select>
|
||||
<div class="status-indicator">
|
||||
<div class="status-dot"></div>
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading service configurations...</p>
|
||||
</div>
|
||||
|
||||
<div class="services-grid" id="servicesGrid">
|
||||
<!-- Service cards will be dynamically inserted here -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="notification" id="notification"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/v1';
|
||||
let SERVICES = [];
|
||||
let serviceConfigs = {};
|
||||
|
||||
// Initialize the app
|
||||
async function init() {
|
||||
showLoading(true);
|
||||
try {
|
||||
await loadServicesFromRegistry();
|
||||
await loadServiceConfigs();
|
||||
renderServices();
|
||||
} catch (error) {
|
||||
showNotification('Failed to load configurations', 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Load services from registry
|
||||
async function loadServicesFromRegistry() {
|
||||
const response = await fetch(`${API_BASE}/registry/services`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch service registry');
|
||||
}
|
||||
|
||||
const registry = await response.json();
|
||||
SERVICES = registry.map(service => ({
|
||||
type: service.id,
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
icon: service.icon || '⚙️',
|
||||
category: service.category,
|
||||
defaultConfig: extractDefaultConfig(service),
|
||||
defaultPricing: extractDefaultPricing(service),
|
||||
capabilities: service.capabilities || []
|
||||
}));
|
||||
}
|
||||
|
||||
// Extract default configuration from service definition
|
||||
function extractDefaultConfig(service) {
|
||||
const config = {};
|
||||
service.input_parameters.forEach(param => {
|
||||
if (param.default !== undefined) {
|
||||
config[param.name] = param.default;
|
||||
} else if (param.type === 'array' && param.options) {
|
||||
config[param.name] = param.options;
|
||||
}
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
// Extract default pricing from service definition
|
||||
function extractDefaultPricing(service) {
|
||||
if (!service.pricing || service.pricing.length === 0) {
|
||||
return { per_unit: 0.01, min_charge: 0.01 };
|
||||
}
|
||||
|
||||
const pricing = {};
|
||||
service.pricing.forEach(tier => {
|
||||
pricing[tier.name] = tier.unit_price;
|
||||
if (tier.min_charge) {
|
||||
pricing.min_charge = tier.min_charge;
|
||||
}
|
||||
});
|
||||
|
||||
return pricing;
|
||||
}
|
||||
|
||||
// Load existing service configurations
|
||||
async function loadServiceConfigs() {
|
||||
const response = await fetch(`${API_BASE}/services/`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch service configs');
|
||||
}
|
||||
|
||||
const configs = await response.json();
|
||||
configs.forEach(config => {
|
||||
serviceConfigs[config.service_type] = config;
|
||||
});
|
||||
}
|
||||
|
||||
// Render service cards
|
||||
function renderServices() {
|
||||
const grid = document.getElementById('servicesGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
const categoryFilter = document.getElementById('categoryFilter').value;
|
||||
const filteredServices = categoryFilter
|
||||
? SERVICES.filter(s => s.category === categoryFilter)
|
||||
: SERVICES;
|
||||
|
||||
filteredServices.forEach(service => {
|
||||
const config = serviceConfigs[service.type] || {
|
||||
service_type: service.type,
|
||||
enabled: false,
|
||||
config: service.defaultConfig,
|
||||
pricing: service.defaultPricing,
|
||||
capabilities: service.capabilities,
|
||||
max_concurrent: 1
|
||||
};
|
||||
|
||||
const card = createServiceCard(service, config);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter services by category
|
||||
function filterByCategory() {
|
||||
renderServices();
|
||||
}
|
||||
|
||||
// Create a service card element
|
||||
function createServiceCard(service, config) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'service-card';
|
||||
card.setAttribute('data-service', service.type);
|
||||
card.innerHTML = `
|
||||
<div class="service-header">
|
||||
<div class="service-icon">${service.icon}</div>
|
||||
<h3 class="service-title">${service.name}</h3>
|
||||
<div class="compatibility-score" id="score-${service.type}" style="display: none;">
|
||||
Score: <span>0</span>/100
|
||||
</div>
|
||||
</div>
|
||||
<p class="service-description">${service.description}</p>
|
||||
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" ${config.enabled ? 'checked' : ''}
|
||||
onchange="toggleService('${service.type}', this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
|
||||
<div class="config-section" id="config-${service.type}"
|
||||
style="display: ${config.enabled ? 'block' : 'none'}">
|
||||
<div class="section-title">
|
||||
⚙️ Configuration
|
||||
</div>
|
||||
|
||||
${renderConfigFields(service.type, config.config)}
|
||||
|
||||
<div class="section-title" style="margin-top: 20px;">
|
||||
💰 Pricing
|
||||
</div>
|
||||
|
||||
${renderPricingFields(service.type, config.pricing)}
|
||||
|
||||
<div class="section-title" style="margin-top: 20px;">
|
||||
⚡ Capacity
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Max Concurrent Jobs</label>
|
||||
<input type="number" min="1" max="10" value="${config.max_concurrent}"
|
||||
onchange="updateConfig('${service.type}', 'max_concurrent', parseInt(this.value)); validateOnChange('${service.type}')">
|
||||
</div>
|
||||
|
||||
<div class="section-title" style="margin-top: 20px;">
|
||||
🎯 Capabilities
|
||||
</div>
|
||||
|
||||
<div class="capabilities-list">
|
||||
${config.capabilities.map(cap =>
|
||||
`<span class="capability-tag">${cap}</span>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="resetConfig('${service.type}')">
|
||||
Reset to Default
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="saveConfig('${service.type}')">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Validate on load if enabled
|
||||
if (config.enabled) {
|
||||
setTimeout(() => validateOnChange(service.type), 100);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Render configuration fields based on service type
|
||||
function renderConfigFields(serviceType, config) {
|
||||
switch (serviceType) {
|
||||
case 'whisper':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Available Models</label>
|
||||
<input type="text" value="${config.models ? config.models.join(', ') : ''}"
|
||||
placeholder="tiny, base, small, medium, large"
|
||||
onchange="updateConfigField('${serviceType}', 'models', this.value.split(',').map(s => s.trim()))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max File Size (MB)</label>
|
||||
<input type="number" value="${config.max_file_size_mb || 500}"
|
||||
onchange="updateConfigField('${serviceType}', 'max_file_size_mb', parseInt(this.value))">
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'stable_diffusion':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Available Models</label>
|
||||
<input type="text" value="${config.models ? config.models.join(', ') : ''}"
|
||||
placeholder="stable-diffusion-1.5, stable-diffusion-2.1, sdxl"
|
||||
onchange="updateConfigField('${serviceType}', 'models', this.value.split(',').map(s => s.trim()))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Resolution</label>
|
||||
<select onchange="updateConfigField('${serviceType}', 'max_resolution', this.value)">
|
||||
<option value="512x512" ${config.max_resolution === '512x512' ? 'selected' : ''}>512x512</option>
|
||||
<option value="768x768" ${config.max_resolution === '768x768' ? 'selected' : ''}>768x768</option>
|
||||
<option value="1024x1024" ${config.max_resolution === '1024x1024' ? 'selected' : ''}>1024x1024</option>
|
||||
<option value="4K" ${config.max_resolution === '4K' ? 'selected' : ''}>4K</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Images per Request</label>
|
||||
<input type="number" min="1" max="10" value="${config.max_images_per_request || 4}"
|
||||
onchange="updateConfigField('${serviceType}', 'max_images_per_request', parseInt(this.value))">
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'llm_inference':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Available Models</label>
|
||||
<input type="text" value="${config.models ? config.models.join(', ') : ''}"
|
||||
placeholder="llama-7b, llama-13b, mistral-7b, mixtral-8x7b"
|
||||
onchange="updateConfigField('${serviceType}', 'models', this.value.split(',').map(s => s.trim()))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Tokens</label>
|
||||
<input type="number" value="${config.max_tokens || 4096}"
|
||||
onchange="updateConfigField('${serviceType}', 'max_tokens', parseInt(this.value))">
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'ffmpeg':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Supported Codecs</label>
|
||||
<input type="text" value="${config.supported_codecs ? config.supported_codecs.join(', ') : ''}"
|
||||
placeholder="h264, h265, vp9"
|
||||
onchange="updateConfigField('${serviceType}', 'supported_codecs', this.value.split(',').map(s => s.trim()))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Resolution</label>
|
||||
<select onchange="updateConfigField('${serviceType}', 'max_resolution', this.value)">
|
||||
<option value="1080p" ${config.max_resolution === '1080p' ? 'selected' : ''}>1080p</option>
|
||||
<option value="4K" ${config.max_resolution === '4K' ? 'selected' : ''}>4K</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max File Size (GB)</label>
|
||||
<input type="number" value="${config.max_file_size_gb || 10}"
|
||||
onchange="updateConfigField('${serviceType}', 'max_file_size_gb', parseInt(this.value))">
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'blender':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Render Engines</label>
|
||||
<input type="text" value="${config.engines ? config.engines.join(', ') : ''}"
|
||||
placeholder="cycles, eevee"
|
||||
onchange="updateConfigField('${serviceType}', 'engines', this.value.split(',').map(s => s.trim()))">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Default Engine</label>
|
||||
<select onchange="updateConfigField('${serviceType}', 'default_engine', this.value)">
|
||||
<option value="cycles" ${config.default_engine === 'cycles' ? 'selected' : ''}>Cycles</option>
|
||||
<option value="eevee" ${config.default_engine === 'eevee' ? 'selected' : ''}>Eevee</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Samples</label>
|
||||
<input type="number" value="${config.max_samples || 4096}"
|
||||
onchange="updateConfigField('${serviceType}', 'max_samples', parseInt(this.value))">
|
||||
</div>
|
||||
`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Render pricing fields based on service type
|
||||
function renderPricingFields(serviceType, pricing) {
|
||||
switch (serviceType) {
|
||||
case 'whisper':
|
||||
case 'llm_inference':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Price per 1k tokens/minutes</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.001" min="0" value="${pricing.per_1k_tokens || pricing.per_minute || 0.001}"
|
||||
onchange="updatePricingField('${serviceType}', '${pricing.per_1k_tokens ? 'per_1k_tokens' : 'per_minute'}', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Minimum Charge</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.01" min="0" value="${pricing.min_charge || 0.01}"
|
||||
onchange="updatePricingField('${serviceType}', 'min_charge', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'stable_diffusion':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Price per Image</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.001" min="0" value="${pricing.per_image || 0.01}"
|
||||
onchange="updatePricingField('${serviceType}', 'per_image', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Price per Step</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.001" min="0" value="${pricing.per_step || 0.001}"
|
||||
onchange="updatePricingField('${serviceType}', 'per_step', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'ffmpeg':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Price per Minute</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.001" min="0" value="${pricing.per_minute || 0.005}"
|
||||
onchange="updatePricingField('${serviceType}', 'per_minute', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Price per GB</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.01" min="0" value="${pricing.per_gb || 0.01}"
|
||||
onchange="updatePricingField('${serviceType}', 'per_gb', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'blender':
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label>Price per Frame</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.001" min="0" value="${pricing.per_frame || 0.01}"
|
||||
onchange="updatePricingField('${serviceType}', 'per_frame', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Price per Hour</label>
|
||||
<div class="price-input-group">
|
||||
<input type="number" step="0.01" min="0" value="${pricing.per_hour || 0.5}"
|
||||
onchange="updatePricingField('${serviceType}', 'per_hour', parseFloat(this.value))">
|
||||
<span class="price-unit">AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle service enabled/disabled
|
||||
function toggleService(serviceType, enabled) {
|
||||
if (!serviceConfigs[serviceType]) {
|
||||
const service = SERVICES.find(s => s.type === serviceType);
|
||||
serviceConfigs[serviceType] = {
|
||||
service_type: serviceType,
|
||||
enabled: enabled,
|
||||
config: service.defaultConfig,
|
||||
pricing: service.defaultPricing,
|
||||
capabilities: service.capabilities,
|
||||
max_concurrent: 1
|
||||
};
|
||||
} else {
|
||||
serviceConfigs[serviceType].enabled = enabled;
|
||||
}
|
||||
|
||||
// Show/hide configuration section
|
||||
const configSection = document.getElementById(`config-${serviceType}`);
|
||||
configSection.style.display = enabled ? 'block' : 'none';
|
||||
|
||||
// Validate when enabling
|
||||
if (enabled) {
|
||||
setTimeout(() => validateOnChange(serviceType), 100);
|
||||
} else {
|
||||
// Clear validation feedback when disabling
|
||||
const card = document.querySelector(`[data-service="${serviceType}"]`);
|
||||
const feedback = card.querySelector('.validation-feedback');
|
||||
if (feedback) feedback.remove();
|
||||
card.style.borderColor = '#e0e0e0';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configuration on change
|
||||
async function validateOnChange(serviceType) {
|
||||
const config = serviceConfigs[serviceType];
|
||||
if (!config || !config.enabled) return;
|
||||
|
||||
const validationResult = await validateService(serviceType, config);
|
||||
showValidationFeedback(serviceType, validationResult);
|
||||
|
||||
// Update score display
|
||||
const scoreElement = document.querySelector(`#score-${serviceType} span`);
|
||||
if (scoreElement) {
|
||||
scoreElement.textContent = validationResult.score;
|
||||
document.getElementById(`score-${serviceType}`).style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Update configuration field
|
||||
function updateConfigField(serviceType, field, value) {
|
||||
if (!serviceConfigs[serviceType]) return;
|
||||
if (!serviceConfigs[serviceType].config) {
|
||||
serviceConfigs[serviceType].config = {};
|
||||
}
|
||||
serviceConfigs[serviceType].config[field] = value;
|
||||
}
|
||||
|
||||
// Update pricing field
|
||||
function updatePricingField(serviceType, field, value) {
|
||||
if (!serviceConfigs[serviceType]) return;
|
||||
if (!serviceConfigs[serviceType].pricing) {
|
||||
serviceConfigs[serviceType].pricing = {};
|
||||
}
|
||||
serviceConfigs[serviceType].pricing[field] = value;
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
function updateConfig(serviceType, field, value) {
|
||||
if (!serviceConfigs[serviceType]) return;
|
||||
serviceConfigs[serviceType][field] = value;
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
async function saveConfig(serviceType) {
|
||||
const config = serviceConfigs[serviceType];
|
||||
if (!config) return;
|
||||
|
||||
// Validate before saving
|
||||
const validationResult = await validateService(serviceType, config);
|
||||
if (!validationResult.valid) {
|
||||
showNotification(`Cannot save: ${validationResult.errors.join(', ')}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/services/${serviceType}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save configuration');
|
||||
}
|
||||
|
||||
showNotification('Configuration saved successfully', 'success');
|
||||
} catch (error) {
|
||||
showNotification('Failed to save configuration: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate service configuration
|
||||
async function validateService(serviceType, config) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/validation/service/${serviceType}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config.config || {})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Validation failed');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { valid: false, errors: [error.message] };
|
||||
}
|
||||
}
|
||||
|
||||
// Show validation feedback in UI
|
||||
function showValidationFeedback(serviceType, validationResult) {
|
||||
const card = document.querySelector(`[data-service="${serviceType}"]`);
|
||||
if (!card) return;
|
||||
|
||||
// Remove existing feedback
|
||||
const existing = card.querySelector('.validation-feedback');
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Create feedback element
|
||||
const feedback = document.createElement('div');
|
||||
feedback.className = 'validation-feedback';
|
||||
|
||||
if (!validationResult.valid) {
|
||||
feedback.innerHTML = `
|
||||
<div class="validation-errors">
|
||||
<strong>❌ Configuration Issues:</strong>
|
||||
<ul>
|
||||
${validationResult.errors.map(e => `<li>${e}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
feedback.style.color = '#f44336';
|
||||
} else if (validationResult.warnings.length > 0) {
|
||||
feedback.innerHTML = `
|
||||
<div class="validation-warnings">
|
||||
<strong>⚠️ Warnings:</strong>
|
||||
<ul>
|
||||
${validationResult.warnings.map(w => `<li>${w}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
feedback.style.color = '#ff9800';
|
||||
} else {
|
||||
feedback.innerHTML = `
|
||||
<div class="validation-success">
|
||||
✅ Configuration is valid (Score: ${validationResult.score}/100)
|
||||
</div>
|
||||
`;
|
||||
feedback.style.color = '#4caf50';
|
||||
}
|
||||
|
||||
// Insert after the toggle switch
|
||||
const toggle = card.querySelector('.toggle-switch');
|
||||
toggle.parentNode.insertBefore(feedback, toggle.nextSibling.nextSibling);
|
||||
|
||||
// Update card border based on validation
|
||||
if (validationResult.valid) {
|
||||
card.style.borderColor = '#4caf50';
|
||||
} else {
|
||||
card.style.borderColor = '#f44336';
|
||||
}
|
||||
}
|
||||
|
||||
// Reset configuration to defaults
|
||||
function resetConfig(serviceType) {
|
||||
const service = SERVICES.find(s => s.type === serviceType);
|
||||
serviceConfigs[serviceType] = {
|
||||
service_type: serviceType,
|
||||
enabled: false,
|
||||
config: service.defaultConfig,
|
||||
pricing: service.defaultPricing,
|
||||
capabilities: service.capabilities,
|
||||
max_concurrent: 1
|
||||
};
|
||||
renderServices();
|
||||
}
|
||||
|
||||
// Get auth headers
|
||||
function getAuthHeaders() {
|
||||
// Get API key from localStorage or other secure storage
|
||||
const apiKey = localStorage.getItem('poolhub_api_key') || '';
|
||||
return {
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
};
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.getElementById('notification');
|
||||
notification.textContent = message;
|
||||
notification.className = `notification ${type}`;
|
||||
notification.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Show/hide loading
|
||||
function showLoading(show) {
|
||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
||||
document.getElementById('servicesGrid').style.display = show ? 'none' : 'grid';
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Dict, List, Optional
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
||||
@ -9,6 +10,15 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class ServiceType(str, Enum):
|
||||
"""Supported service types"""
|
||||
WHISPER = "whisper"
|
||||
STABLE_DIFFUSION = "stable_diffusion"
|
||||
LLM_INFERENCE = "llm_inference"
|
||||
FFMPEG = "ffmpeg"
|
||||
BLENDER = "blender"
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
@ -93,3 +103,26 @@ class Feedback(Base):
|
||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=dt.datetime.utcnow)
|
||||
|
||||
miner: Mapped[Miner] = relationship(back_populates="feedback")
|
||||
|
||||
|
||||
class ServiceConfig(Base):
|
||||
"""Service configuration for a miner"""
|
||||
__tablename__ = "service_configs"
|
||||
|
||||
id: Mapped[PGUUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||
miner_id: Mapped[str] = mapped_column(ForeignKey("miners.miner_id", ondelete="CASCADE"), nullable=False)
|
||||
service_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
config: Mapped[Dict[str, Any]] = mapped_column(JSONB, default=dict)
|
||||
pricing: Mapped[Dict[str, Any]] = mapped_column(JSONB, default=dict)
|
||||
capabilities: Mapped[List[str]] = mapped_column(JSONB, default=list)
|
||||
max_concurrent: Mapped[int] = mapped_column(Integer, default=1)
|
||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=dt.datetime.utcnow)
|
||||
updated_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=dt.datetime.utcnow, onupdate=dt.datetime.utcnow)
|
||||
|
||||
# Add unique constraint for miner_id + service_type
|
||||
__table_args__ = (
|
||||
{"schema": None},
|
||||
)
|
||||
|
||||
miner: Mapped[Miner] = relationship(backref="service_configs")
|
||||
|
||||
308
apps/pool-hub/src/poolhub/services/validation.py
Normal file
308
apps/pool-hub/src/poolhub/services/validation.py
Normal file
@ -0,0 +1,308 @@
|
||||
"""
|
||||
Hardware validation service for service configurations
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import requests
|
||||
from ..models import Miner
|
||||
from ..settings import settings
|
||||
|
||||
|
||||
class ValidationResult:
|
||||
"""Validation result for a service configuration"""
|
||||
def __init__(self):
|
||||
self.valid = True
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
self.score = 0 # 0-100 score indicating how well the hardware matches
|
||||
self.missing_requirements = []
|
||||
self.performance_impact = None
|
||||
|
||||
|
||||
class HardwareValidator:
|
||||
"""Validates service configurations against miner hardware"""
|
||||
|
||||
def __init__(self):
|
||||
self.registry_url = f"{settings.coordinator_url}/v1/registry"
|
||||
|
||||
async def validate_service_for_miner(
|
||||
self,
|
||||
miner: Miner,
|
||||
service_id: str,
|
||||
config: Dict[str, Any]
|
||||
) -> ValidationResult:
|
||||
"""Validate if a miner can run a specific service"""
|
||||
result = ValidationResult()
|
||||
|
||||
try:
|
||||
# Get service definition from registry
|
||||
service = await self._get_service_definition(service_id)
|
||||
if not service:
|
||||
result.valid = False
|
||||
result.errors.append(f"Service {service_id} not found")
|
||||
return result
|
||||
|
||||
# Check hardware requirements
|
||||
hw_result = self._check_hardware_requirements(miner, service)
|
||||
result.errors.extend(hw_result.errors)
|
||||
result.warnings.extend(hw_result.warnings)
|
||||
result.score = hw_result.score
|
||||
result.missing_requirements = hw_result.missing_requirements
|
||||
|
||||
# Check configuration parameters
|
||||
config_result = self._check_configuration_parameters(service, config)
|
||||
result.errors.extend(config_result.errors)
|
||||
result.warnings.extend(config_result.warnings)
|
||||
|
||||
# Calculate performance impact
|
||||
result.performance_impact = self._estimate_performance_impact(miner, service, config)
|
||||
|
||||
# Overall validity
|
||||
result.valid = len(result.errors) == 0
|
||||
|
||||
except Exception as e:
|
||||
result.valid = False
|
||||
result.errors.append(f"Validation error: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
async def _get_service_definition(self, service_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch service definition from registry"""
|
||||
try:
|
||||
response = requests.get(f"{self.registry_url}/services/{service_id}")
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _check_hardware_requirements(
|
||||
self,
|
||||
miner: Miner,
|
||||
service: Dict[str, Any]
|
||||
) -> ValidationResult:
|
||||
"""Check if miner meets hardware requirements"""
|
||||
result = ValidationResult()
|
||||
requirements = service.get("requirements", [])
|
||||
|
||||
for req in requirements:
|
||||
component = req["component"]
|
||||
min_value = req["min_value"]
|
||||
recommended = req.get("recommended")
|
||||
unit = req.get("unit", "")
|
||||
|
||||
# Map component to miner attributes
|
||||
miner_value = self._get_miner_hardware_value(miner, component)
|
||||
if miner_value is None:
|
||||
result.warnings.append(f"Cannot verify {component} requirement")
|
||||
continue
|
||||
|
||||
# Check minimum requirement
|
||||
if not self._meets_requirement(miner_value, min_value, component):
|
||||
result.valid = False
|
||||
result.errors.append(
|
||||
f"Insufficient {component}: have {miner_value}{unit}, need {min_value}{unit}"
|
||||
)
|
||||
result.missing_requirements.append({
|
||||
"component": component,
|
||||
"have": miner_value,
|
||||
"need": min_value,
|
||||
"unit": unit
|
||||
})
|
||||
# Check against recommended
|
||||
elif recommended and not self._meets_requirement(miner_value, recommended, component):
|
||||
result.warnings.append(
|
||||
f"{component} below recommended: have {miner_value}{unit}, recommended {recommended}{unit}"
|
||||
)
|
||||
result.score -= 10 # Penalize for below recommended
|
||||
|
||||
# Calculate base score
|
||||
result.score = max(0, 100 - len(result.errors) * 20 - len(result.warnings) * 5)
|
||||
|
||||
return result
|
||||
|
||||
def _get_miner_hardware_value(self, miner: Miner, component: str) -> Optional[float]:
|
||||
"""Get hardware value from miner model"""
|
||||
mapping = {
|
||||
"gpu": 1 if miner.gpu_name else 0, # Binary: has GPU or not
|
||||
"vram": miner.gpu_vram_gb,
|
||||
"cpu": miner.cpu_cores,
|
||||
"ram": miner.ram_gb,
|
||||
"storage": 100, # Assume sufficient storage
|
||||
"cuda": self._get_cuda_version(miner),
|
||||
"network": 1, # Assume network is available
|
||||
}
|
||||
return mapping.get(component)
|
||||
|
||||
def _get_cuda_version(self, miner: Miner) -> float:
|
||||
"""Extract CUDA version from capabilities or tags"""
|
||||
# Check tags for CUDA version
|
||||
for tag, value in miner.tags.items():
|
||||
if tag.lower() == "cuda":
|
||||
# Extract version number (e.g., "11.8" -> 11.8)
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return 0.0 # No CUDA info
|
||||
|
||||
def _meets_requirement(self, have: float, need: float, component: str) -> bool:
|
||||
"""Check if hardware meets requirement"""
|
||||
if component == "gpu":
|
||||
return have >= need # Both are 0 or 1
|
||||
return have >= need
|
||||
|
||||
def _check_configuration_parameters(
|
||||
self,
|
||||
service: Dict[str, Any],
|
||||
config: Dict[str, Any]
|
||||
) -> ValidationResult:
|
||||
"""Check if configuration parameters are valid"""
|
||||
result = ValidationResult()
|
||||
input_params = service.get("input_parameters", [])
|
||||
|
||||
# Check for required parameters
|
||||
required_params = {p["name"] for p in input_params if p.get("required", True)}
|
||||
provided_params = set(config.keys())
|
||||
|
||||
missing = required_params - provided_params
|
||||
if missing:
|
||||
result.errors.extend([f"Missing required parameter: {p}" for p in missing])
|
||||
|
||||
# Validate parameter values
|
||||
for param in input_params:
|
||||
name = param["name"]
|
||||
if name not in config:
|
||||
continue
|
||||
|
||||
value = config[name]
|
||||
param_type = param.get("type")
|
||||
|
||||
# Type validation
|
||||
if param_type == "integer" and not isinstance(value, int):
|
||||
result.errors.append(f"Parameter {name} must be an integer")
|
||||
elif param_type == "float" and not isinstance(value, (int, float)):
|
||||
result.errors.append(f"Parameter {name} must be a number")
|
||||
elif param_type == "array" and not isinstance(value, list):
|
||||
result.errors.append(f"Parameter {name} must be an array")
|
||||
|
||||
# Value constraints
|
||||
if "min_value" in param and value < param["min_value"]:
|
||||
result.errors.append(
|
||||
f"Parameter {name} must be >= {param['min_value']}"
|
||||
)
|
||||
if "max_value" in param and value > param["max_value"]:
|
||||
result.errors.append(
|
||||
f"Parameter {name} must be <= {param['max_value']}"
|
||||
)
|
||||
if "options" in param and value not in param["options"]:
|
||||
result.errors.append(
|
||||
f"Parameter {name} must be one of: {', '.join(param['options'])}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _estimate_performance_impact(
|
||||
self,
|
||||
miner: Miner,
|
||||
service: Dict[str, Any],
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Estimate performance impact based on hardware and configuration"""
|
||||
impact = {
|
||||
"level": "low", # low, medium, high
|
||||
"expected_fps": None,
|
||||
"expected_throughput": None,
|
||||
"bottleneck": None,
|
||||
"recommendations": []
|
||||
}
|
||||
|
||||
# Analyze based on service type
|
||||
service_id = service["id"]
|
||||
|
||||
if service_id in ["stable_diffusion", "image_generation"]:
|
||||
# Image generation performance
|
||||
if miner.gpu_vram_gb < 8:
|
||||
impact["level"] = "high"
|
||||
impact["bottleneck"] = "VRAM"
|
||||
impact["expected_fps"] = "0.1-0.5 images/sec"
|
||||
elif miner.gpu_vram_gb < 16:
|
||||
impact["level"] = "medium"
|
||||
impact["expected_fps"] = "0.5-2 images/sec"
|
||||
else:
|
||||
impact["level"] = "low"
|
||||
impact["expected_fps"] = "2-5 images/sec"
|
||||
|
||||
elif service_id in ["llm_inference"]:
|
||||
# LLM inference performance
|
||||
if miner.gpu_vram_gb < 8:
|
||||
impact["level"] = "high"
|
||||
impact["bottleneck"] = "VRAM"
|
||||
impact["expected_throughput"] = "1-5 tokens/sec"
|
||||
elif miner.gpu_vram_gb < 16:
|
||||
impact["level"] = "medium"
|
||||
impact["expected_throughput"] = "5-20 tokens/sec"
|
||||
else:
|
||||
impact["level"] = "low"
|
||||
impact["expected_throughput"] = "20-50+ tokens/sec"
|
||||
|
||||
elif service_id in ["video_transcoding", "ffmpeg"]:
|
||||
# Video transcoding performance
|
||||
if miner.gpu_vram_gb < 4:
|
||||
impact["level"] = "high"
|
||||
impact["bottleneck"] = "GPU Memory"
|
||||
impact["expected_fps"] = "10-30 fps (720p)"
|
||||
elif miner.gpu_vram_gb < 8:
|
||||
impact["level"] = "medium"
|
||||
impact["expected_fps"] = "30-60 fps (1080p)"
|
||||
else:
|
||||
impact["level"] = "low"
|
||||
impact["expected_fps"] = "60+ fps (4K)"
|
||||
|
||||
elif service_id in ["3d_rendering", "blender"]:
|
||||
# 3D rendering performance
|
||||
if miner.gpu_vram_gb < 8:
|
||||
impact["level"] = "high"
|
||||
impact["bottleneck"] = "VRAM"
|
||||
impact["expected_throughput"] = "0.01-0.1 samples/sec"
|
||||
elif miner.gpu_vram_gb < 16:
|
||||
impact["level"] = "medium"
|
||||
impact["expected_throughput"] = "0.1-1 samples/sec"
|
||||
else:
|
||||
impact["level"] = "low"
|
||||
impact["expected_throughput"] = "1-5+ samples/sec"
|
||||
|
||||
# Add recommendations based on bottlenecks
|
||||
if impact["bottleneck"] == "VRAM":
|
||||
impact["recommendations"].append("Consider upgrading GPU with more VRAM")
|
||||
impact["recommendations"].append("Reduce batch size or resolution")
|
||||
elif impact["bottleneck"] == "GPU Memory":
|
||||
impact["recommendations"].append("Use GPU acceleration if available")
|
||||
impact["recommendations"].append("Lower resolution or bitrate settings")
|
||||
|
||||
return impact
|
||||
|
||||
async def get_compatible_services(self, miner: Miner) -> List[Tuple[str, int]]:
|
||||
"""Get list of services compatible with miner hardware"""
|
||||
try:
|
||||
# Get all services from registry
|
||||
response = requests.get(f"{self.registry_url}/services")
|
||||
if response.status_code != 200:
|
||||
return []
|
||||
|
||||
services = response.json()
|
||||
compatible = []
|
||||
|
||||
for service in services:
|
||||
service_id = service["id"]
|
||||
# Quick validation without config
|
||||
result = await self.validate_service_for_miner(miner, service_id, {})
|
||||
if result.valid:
|
||||
compatible.append((service_id, result.score))
|
||||
|
||||
# Sort by score (best match first)
|
||||
compatible.sort(key=lambda x: x[1], reverse=True)
|
||||
return compatible
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
Reference in New Issue
Block a user