GT AI OS Community Edition v2.0.33
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>
This commit is contained in:
238
apps/tenant-backend/app/api/v1/optics.py
Normal file
238
apps/tenant-backend/app/api/v1/optics.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
Optics Cost Tracking API Endpoints
|
||||
|
||||
Provides cost visibility for inference and storage usage.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
|
||||
from app.core.postgresql_client import get_postgresql_client
|
||||
from app.api.v1.observability import get_current_user, get_user_role
|
||||
|
||||
from app.services.optics_service import (
|
||||
fetch_optics_settings,
|
||||
get_optics_cost_summary,
|
||||
STORAGE_COST_PER_MB_CENTS
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/optics", tags=["Optics Cost Tracking"])
|
||||
|
||||
|
||||
# Response models
|
||||
class OpticsSettingsResponse(BaseModel):
|
||||
enabled: bool
|
||||
storage_cost_per_mb_cents: float
|
||||
show_to_admins_only: bool = True
|
||||
|
||||
|
||||
class ModelCostBreakdown(BaseModel):
|
||||
model_id: str
|
||||
model_name: str
|
||||
tokens: int
|
||||
conversations: int
|
||||
messages: int
|
||||
cost_cents: float
|
||||
cost_display: str
|
||||
percentage: float
|
||||
|
||||
|
||||
class UserCostBreakdown(BaseModel):
|
||||
user_id: str
|
||||
email: str
|
||||
tokens: int
|
||||
cost_cents: float
|
||||
cost_display: str
|
||||
percentage: float
|
||||
|
||||
|
||||
class OpticsCostResponse(BaseModel):
|
||||
enabled: bool
|
||||
inference_cost_cents: float
|
||||
storage_cost_cents: float
|
||||
total_cost_cents: float
|
||||
inference_cost_display: str
|
||||
storage_cost_display: str
|
||||
total_cost_display: str
|
||||
total_tokens: int
|
||||
total_storage_mb: float
|
||||
document_count: int
|
||||
dataset_count: int
|
||||
by_model: List[ModelCostBreakdown]
|
||||
by_user: Optional[List[UserCostBreakdown]] = None
|
||||
period_start: str
|
||||
period_end: str
|
||||
|
||||
|
||||
@router.get("/settings", response_model=OpticsSettingsResponse)
|
||||
async def get_optics_settings(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Check if Optics is enabled for the current tenant.
|
||||
|
||||
This endpoint is used by the frontend to determine whether
|
||||
to show the Optics tab in the observability dashboard.
|
||||
"""
|
||||
tenant_domain = current_user.get("tenant_domain", "test-company")
|
||||
|
||||
try:
|
||||
settings = await fetch_optics_settings(tenant_domain)
|
||||
|
||||
return OpticsSettingsResponse(
|
||||
enabled=settings.get("enabled", False),
|
||||
storage_cost_per_mb_cents=settings.get("storage_cost_per_mb_cents", STORAGE_COST_PER_MB_CENTS),
|
||||
show_to_admins_only=True # Only admins can see user breakdown
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching optics settings: {str(e)}")
|
||||
return OpticsSettingsResponse(
|
||||
enabled=False,
|
||||
storage_cost_per_mb_cents=STORAGE_COST_PER_MB_CENTS,
|
||||
show_to_admins_only=True
|
||||
)
|
||||
|
||||
|
||||
@router.get("/costs", response_model=OpticsCostResponse)
|
||||
async def get_optics_costs(
|
||||
days: Optional[int] = Query(30, ge=1, le=365, description="Number of days to look back"),
|
||||
start_date: Optional[str] = Query(None, description="Custom start date (ISO format)"),
|
||||
end_date: Optional[str] = Query(None, description="Custom end date (ISO format)"),
|
||||
user_id: Optional[str] = Query(None, description="Filter by user ID (admin only)"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get Optics cost breakdown for the current tenant.
|
||||
|
||||
Returns inference costs calculated from token usage and model pricing,
|
||||
plus storage costs at the configured rate (default 4 cents/MB).
|
||||
|
||||
- **days**: Number of days to look back (default 30)
|
||||
- **start_date**: Custom start date (overrides days)
|
||||
- **end_date**: Custom end date
|
||||
- **user_id**: Filter by specific user (admin only)
|
||||
"""
|
||||
tenant_domain = current_user.get("tenant_domain", "test-company")
|
||||
|
||||
# Check if Optics is enabled
|
||||
settings = await fetch_optics_settings(tenant_domain)
|
||||
if not settings.get("enabled", False):
|
||||
return OpticsCostResponse(
|
||||
enabled=False,
|
||||
inference_cost_cents=0,
|
||||
storage_cost_cents=0,
|
||||
total_cost_cents=0,
|
||||
inference_cost_display="$0.00",
|
||||
storage_cost_display="$0.00",
|
||||
total_cost_display="$0.00",
|
||||
total_tokens=0,
|
||||
total_storage_mb=0,
|
||||
document_count=0,
|
||||
dataset_count=0,
|
||||
by_model=[],
|
||||
by_user=None,
|
||||
period_start=datetime.utcnow().isoformat(),
|
||||
period_end=datetime.utcnow().isoformat()
|
||||
)
|
||||
|
||||
pg_client = await get_postgresql_client()
|
||||
|
||||
# Get user role for permission checks
|
||||
user_email = current_user.get("email", "")
|
||||
user_role = await get_user_role(pg_client, user_email, tenant_domain)
|
||||
|
||||
is_admin = user_role in ["admin", "developer"]
|
||||
|
||||
# Handle user filter - only admins can filter by user
|
||||
filter_user_id = None
|
||||
if user_id:
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can filter by user"
|
||||
)
|
||||
filter_user_id = user_id
|
||||
elif not is_admin:
|
||||
# Non-admins can only see their own data
|
||||
# Get user UUID from email
|
||||
user_query = f"""
|
||||
SELECT id FROM tenant_{tenant_domain.replace('-', '_')}.users
|
||||
WHERE email = $1 LIMIT 1
|
||||
"""
|
||||
user_result = await pg_client.execute_query(user_query, user_email)
|
||||
if user_result:
|
||||
filter_user_id = str(user_result[0]["id"])
|
||||
|
||||
# Calculate date range
|
||||
date_end = datetime.utcnow()
|
||||
date_start = date_end - timedelta(days=days)
|
||||
|
||||
if start_date:
|
||||
try:
|
||||
date_start = datetime.fromisoformat(start_date.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid start_date format. Use ISO format."
|
||||
)
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
date_end = datetime.fromisoformat(end_date.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid end_date format. Use ISO format."
|
||||
)
|
||||
|
||||
try:
|
||||
cost_summary = await get_optics_cost_summary(
|
||||
pg_client=pg_client,
|
||||
tenant_domain=tenant_domain,
|
||||
date_start=date_start,
|
||||
date_end=date_end,
|
||||
user_id=filter_user_id,
|
||||
include_user_breakdown=is_admin and not filter_user_id # Only include breakdown for platform view
|
||||
)
|
||||
|
||||
# Convert to response model
|
||||
by_model = [
|
||||
ModelCostBreakdown(**item)
|
||||
for item in cost_summary.get("by_model", [])
|
||||
]
|
||||
|
||||
by_user = None
|
||||
if cost_summary.get("by_user"):
|
||||
by_user = [
|
||||
UserCostBreakdown(**item)
|
||||
for item in cost_summary["by_user"]
|
||||
]
|
||||
|
||||
return OpticsCostResponse(
|
||||
enabled=True,
|
||||
inference_cost_cents=cost_summary["inference_cost_cents"],
|
||||
storage_cost_cents=cost_summary["storage_cost_cents"],
|
||||
total_cost_cents=cost_summary["total_cost_cents"],
|
||||
inference_cost_display=cost_summary["inference_cost_display"],
|
||||
storage_cost_display=cost_summary["storage_cost_display"],
|
||||
total_cost_display=cost_summary["total_cost_display"],
|
||||
total_tokens=cost_summary["total_tokens"],
|
||||
total_storage_mb=cost_summary["total_storage_mb"],
|
||||
document_count=cost_summary["document_count"],
|
||||
dataset_count=cost_summary["dataset_count"],
|
||||
by_model=by_model,
|
||||
by_user=by_user,
|
||||
period_start=cost_summary["period_start"],
|
||||
period_end=cost_summary["period_end"]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating optics costs: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to calculate costs"
|
||||
)
|
||||
Reference in New Issue
Block a user