Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
209 lines
8.9 KiB
Python
209 lines
8.9 KiB
Python
"""
|
|
Model Configuration Database Schema for GT 2.0 Admin Control Panel
|
|
|
|
This model stores configurations for all AI models across the GT 2.0 platform.
|
|
Configurations are synced to resource clusters via RabbitMQ messages.
|
|
"""
|
|
|
|
from sqlalchemy import Column, String, JSON, Boolean, DateTime, Float, Integer, Text, UniqueConstraint
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
import uuid
|
|
from app.core.database import Base
|
|
|
|
|
|
class ModelConfig(Base):
|
|
"""Model configuration stored in PostgreSQL admin database"""
|
|
__tablename__ = "model_configs"
|
|
|
|
# Primary key - UUID
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
|
|
# Business identifier - unique per provider (same model_id can exist for different providers)
|
|
model_id = Column(String(255), nullable=False, index=True)
|
|
name = Column(String(255), nullable=False)
|
|
version = Column(String(50), default="1.0")
|
|
|
|
# Provider information
|
|
provider = Column(String(50), nullable=False) # groq, external, openai, anthropic, nvidia
|
|
model_type = Column(String(50), nullable=False) # llm, embedding, audio, tts, vision
|
|
|
|
# Endpoint configuration
|
|
endpoint = Column(String(500), nullable=False)
|
|
api_key_name = Column(String(100)) # Environment variable name for API key
|
|
|
|
# Model specifications
|
|
context_window = Column(Integer)
|
|
max_tokens = Column(Integer)
|
|
dimensions = Column(Integer) # For embedding models
|
|
|
|
# Capabilities (JSON object)
|
|
capabilities = Column(JSON, default={})
|
|
|
|
# Cost information (per million tokens, as per Groq pricing)
|
|
cost_per_million_input = Column(Float, default=0.0)
|
|
cost_per_million_output = Column(Float, default=0.0)
|
|
|
|
# Configuration and metadata
|
|
description = Column(Text)
|
|
config = Column(JSON, default={}) # Additional provider-specific config
|
|
|
|
# Status and health
|
|
is_active = Column(Boolean, default=True)
|
|
health_status = Column(String(20), default="unknown") # healthy, unhealthy, unknown
|
|
last_health_check = Column(DateTime)
|
|
|
|
# Compound model flag (for pass-through pricing based on actual usage)
|
|
is_compound = Column(Boolean, default=False)
|
|
|
|
# Usage tracking (will be updated from resource clusters)
|
|
request_count = Column(Integer, default=0)
|
|
error_count = Column(Integer, default=0)
|
|
success_rate = Column(Float, default=100.0)
|
|
avg_latency_ms = Column(Float, default=0.0)
|
|
|
|
# Tenant access control (JSON array)
|
|
# Example: {"allowed_tenants": ["tenant1", "tenant2"], "blocked_tenants": [], "global_access": true}
|
|
tenant_restrictions = Column(JSON, default=lambda: {"global_access": True})
|
|
|
|
# Required capabilities to use this model (JSON array)
|
|
# Example: ["llm:execute", "advanced:reasoning", "vision:analyze"]
|
|
required_capabilities = Column(JSON, default=list)
|
|
|
|
# Lifecycle timestamps
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
# Relationships
|
|
tenant_configs = relationship("TenantModelConfig", back_populates="model_config", cascade="all, delete-orphan")
|
|
|
|
# Unique constraint: same model_id can exist for different providers
|
|
__table_args__ = (
|
|
UniqueConstraint('model_id', 'provider', name='model_configs_model_id_provider_unique'),
|
|
)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert model to dictionary for API responses"""
|
|
return {
|
|
"id": str(self.id) if self.id else None,
|
|
"model_id": self.model_id,
|
|
"name": self.name,
|
|
"version": self.version,
|
|
"provider": self.provider,
|
|
"model_type": self.model_type,
|
|
"endpoint": self.endpoint,
|
|
"api_key_name": self.api_key_name,
|
|
"specifications": {
|
|
"context_window": self.context_window,
|
|
"max_tokens": self.max_tokens,
|
|
"dimensions": self.dimensions,
|
|
},
|
|
"capabilities": self.capabilities or {},
|
|
"cost": {
|
|
"per_million_input": self.cost_per_million_input,
|
|
"per_million_output": self.cost_per_million_output,
|
|
},
|
|
"description": self.description,
|
|
"config": self.config or {},
|
|
"status": {
|
|
"is_active": self.is_active,
|
|
"is_compound": self.is_compound,
|
|
"health_status": self.health_status,
|
|
"last_health_check": self.last_health_check.isoformat() if self.last_health_check else None,
|
|
},
|
|
"usage": {
|
|
"request_count": self.request_count,
|
|
"error_count": self.error_count,
|
|
"success_rate": self.success_rate,
|
|
"avg_latency_ms": self.avg_latency_ms,
|
|
},
|
|
"access_control": {
|
|
"tenant_restrictions": self.tenant_restrictions or {},
|
|
"required_capabilities": self.required_capabilities or [],
|
|
},
|
|
"timestamps": {
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
}
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> 'ModelConfig':
|
|
"""Create ModelConfig from dictionary"""
|
|
# Handle both nested and flat data formats
|
|
specifications = data.get("specifications", {})
|
|
cost = data.get("cost", {})
|
|
status = data.get("status", {})
|
|
access_control = data.get("access_control", {})
|
|
|
|
return cls(
|
|
model_id=data.get("model_id"),
|
|
name=data.get("name"),
|
|
version=data.get("version", "1.0"),
|
|
provider=data.get("provider"),
|
|
model_type=data.get("model_type"),
|
|
endpoint=data.get("endpoint"),
|
|
api_key_name=data.get("api_key_name"),
|
|
# Handle both nested and flat context_window/max_tokens with type conversion
|
|
context_window=int(specifications.get("context_window") or data.get("context_window", 0)) if (specifications.get("context_window") or data.get("context_window")) else None,
|
|
max_tokens=int(specifications.get("max_tokens") or data.get("max_tokens", 0)) if (specifications.get("max_tokens") or data.get("max_tokens")) else None,
|
|
dimensions=int(specifications.get("dimensions") or data.get("dimensions", 0)) if (specifications.get("dimensions") or data.get("dimensions")) else None,
|
|
capabilities=data.get("capabilities", {}),
|
|
# Handle both nested and flat cost fields with type conversion
|
|
cost_per_million_input=float(cost.get("per_million_input") or data.get("cost_per_million_input", 0.0)),
|
|
cost_per_million_output=float(cost.get("per_million_output") or data.get("cost_per_million_output", 0.0)),
|
|
description=data.get("description"),
|
|
config=data.get("config", {}),
|
|
# Handle both nested and flat is_active
|
|
is_active=status.get("is_active") if status.get("is_active") is not None else data.get("is_active", True),
|
|
# Handle both nested and flat is_compound
|
|
is_compound=status.get("is_compound") if status.get("is_compound") is not None else data.get("is_compound", False),
|
|
tenant_restrictions=access_control.get("tenant_restrictions", data.get("tenant_restrictions", {"global_access": True})),
|
|
required_capabilities=access_control.get("required_capabilities", data.get("required_capabilities", [])),
|
|
)
|
|
|
|
|
|
class ModelUsageLog(Base):
|
|
"""Log of model usage events from resource clusters"""
|
|
__tablename__ = "model_usage_logs"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
model_id = Column(String(255), nullable=False, index=True)
|
|
tenant_id = Column(String(100), nullable=False, index=True)
|
|
user_id = Column(String(100), nullable=False)
|
|
|
|
# Usage metrics
|
|
tokens_input = Column(Integer, default=0)
|
|
tokens_output = Column(Integer, default=0)
|
|
tokens_total = Column(Integer, default=0)
|
|
cost = Column(Float, default=0.0)
|
|
latency_ms = Column(Float)
|
|
|
|
# Request metadata
|
|
success = Column(Boolean, default=True)
|
|
error_message = Column(Text)
|
|
request_id = Column(String(100))
|
|
|
|
# Timestamp
|
|
timestamp = Column(DateTime, default=func.now())
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary"""
|
|
return {
|
|
"id": self.id,
|
|
"model_id": self.model_id,
|
|
"tenant_id": self.tenant_id,
|
|
"user_id": self.user_id,
|
|
"tokens": {
|
|
"input": self.tokens_input,
|
|
"output": self.tokens_output,
|
|
"total": self.tokens_total,
|
|
},
|
|
"cost": self.cost,
|
|
"latency_ms": self.latency_ms,
|
|
"success": self.success,
|
|
"error_message": self.error_message,
|
|
"request_id": self.request_id,
|
|
"timestamp": self.timestamp.isoformat(),
|
|
} |