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:
oib
2025-12-22 10:33:23 +01:00
parent d98b2c7772
commit c8be9d7414
260 changed files with 59033 additions and 351 deletions

View File

@ -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:

View 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

View 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})

View 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

View File

@ -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

View 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>

View File

@ -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")

View 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 []