From 10cdf06ae1d14a427652bae28ea68bb89e4b7052 Mon Sep 17 00:00:00 2001 From: Aljaz Ceru Date: Tue, 26 Aug 2025 09:11:22 +0200 Subject: [PATCH] migration of chatbot api to openai compatibility --- backend/app/api/public_v1/__init__.py | 6 +- backend/app/api/v1/__init__.py | 4 - backend/app/api/v1/chatbot.py | 346 +++++++++++- backend/app/api/v1/tee.py | 334 ------------ backend/app/models/api_key.py | 3 +- backend/app/services/tee_service.py | 363 ------------ frontend/src/app/llm/page.tsx | 115 +++- frontend/src/app/playground/page.tsx | 8 +- frontend/src/app/settings/page.tsx | 9 +- .../src/components/chatbot/ChatInterface.tsx | 11 +- .../src/components/chatbot/ChatbotManager.tsx | 27 +- .../src/components/playground/TEEMonitor.tsx | 515 ------------------ 12 files changed, 495 insertions(+), 1246 deletions(-) delete mode 100644 backend/app/api/v1/tee.py delete mode 100644 backend/app/services/tee_service.py delete mode 100644 frontend/src/components/playground/TEEMonitor.tsx diff --git a/backend/app/api/public_v1/__init__.py b/backend/app/api/public_v1/__init__.py index f704605..265da05 100644 --- a/backend/app/api/public_v1/__init__.py +++ b/backend/app/api/public_v1/__init__.py @@ -5,7 +5,6 @@ Public API v1 package - for external clients from fastapi import APIRouter from ..v1.llm import router as llm_router from ..v1.chatbot import router as chatbot_router -from ..v1.tee import router as tee_router from ..v1.openai_compat import router as openai_router # Create public API router @@ -18,7 +17,4 @@ public_api_router.include_router(openai_router, tags=["openai-compat"]) public_api_router.include_router(llm_router, prefix="/llm", tags=["public-llm"]) # Include public chatbot API (external chatbot integrations) -public_api_router.include_router(chatbot_router, prefix="/chatbot", tags=["public-chatbot"]) - -# Include TEE routes (public TEE services if applicable) -public_api_router.include_router(tee_router, prefix="/tee", tags=["public-tee"]) \ No newline at end of file +public_api_router.include_router(chatbot_router, prefix="/chatbot", tags=["public-chatbot"]) \ No newline at end of file diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index 0fb8fe3..6f66641 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -5,7 +5,6 @@ API v1 package from fastapi import APIRouter from .auth import router as auth_router from .llm import router as llm_router -from .tee import router as tee_router from .modules import router as modules_router from .platform import router as platform_router from .users import router as users_router @@ -29,9 +28,6 @@ api_router.include_router(auth_router, prefix="/auth", tags=["authentication"]) # Include LLM proxy routes api_router.include_router(llm_router, prefix="/llm", tags=["llm"]) -# Include TEE routes -api_router.include_router(tee_router, prefix="/tee", tags=["tee"]) - # Include modules routes api_router.include_router(modules_router, prefix="/modules", tags=["modules"]) diff --git a/backend/app/api/v1/chatbot.py b/backend/app/api/v1/chatbot.py index 26dd314..afc66e9 100644 --- a/backend/app/api/v1/chatbot.py +++ b/backend/app/api/v1/chatbot.py @@ -3,9 +3,10 @@ Chatbot API endpoints """ import asyncio +import time from typing import Dict, Any, List, Optional from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, delete from datetime import datetime @@ -42,6 +43,44 @@ class ChatRequest(BaseModel): conversation_id: Optional[str] = None +# OpenAI-compatible models +class ChatMessage(BaseModel): + role: str = Field(..., description="Message role (system, user, assistant)") + content: str = Field(..., description="Message content") + + +class ChatbotChatCompletionRequest(BaseModel): + messages: List[ChatMessage] = Field(..., description="List of messages") + max_tokens: Optional[int] = Field(None, description="Maximum tokens to generate") + temperature: Optional[float] = Field(None, description="Temperature for sampling") + top_p: Optional[float] = Field(None, description="Top-p sampling parameter") + frequency_penalty: Optional[float] = Field(None, description="Frequency penalty") + presence_penalty: Optional[float] = Field(None, description="Presence penalty") + stop: Optional[List[str]] = Field(None, description="Stop sequences") + stream: Optional[bool] = Field(False, description="Stream response") + + +class ChatChoice(BaseModel): + index: int + message: ChatMessage + finish_reason: str + + +class ChatUsage(BaseModel): + prompt_tokens: int + completion_tokens: int + total_tokens: int + + +class ChatbotChatCompletionResponse(BaseModel): + id: str + object: str = "chat.completion" + created: int + model: str + choices: List[ChatChoice] + usage: ChatUsage + + @router.get("/list") @router.get("/instances") async def list_chatbots( @@ -330,6 +369,151 @@ async def chat_with_chatbot( raise HTTPException(status_code=500, detail=f"Failed to process chat: {str(e)}") +@router.post("/{chatbot_id}/chat/completions", response_model=ChatbotChatCompletionResponse) +async def chatbot_chat_completions( + chatbot_id: str, + request: ChatbotChatCompletionRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """OpenAI-compatible chat completions endpoint for chatbot""" + user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id + log_api_request("chatbot_chat_completions", { + "user_id": user_id, + "chatbot_id": chatbot_id, + "messages_count": len(request.messages) + }) + + try: + # Get the chatbot instance + result = await db.execute( + select(ChatbotInstance) + .where(ChatbotInstance.id == chatbot_id) + .where(ChatbotInstance.created_by == str(user_id)) + ) + chatbot = result.scalar_one_or_none() + + if not chatbot: + raise HTTPException(status_code=404, detail="Chatbot not found") + + if not chatbot.is_active: + raise HTTPException(status_code=400, detail="Chatbot is not active") + + # Find the last user message to extract conversation context + user_messages = [msg for msg in request.messages if msg.role == "user"] + if not user_messages: + raise HTTPException(status_code=400, detail="No user message found in conversation") + + last_user_message = user_messages[-1].content + + # Initialize conversation service + conversation_service = ConversationService(db) + + # For OpenAI format, we'll try to find an existing conversation or create a new one + # We'll use a simple hash of the conversation messages as the conversation identifier + import hashlib + conv_hash = hashlib.md5(str([f"{msg.role}:{msg.content}" for msg in request.messages]).encode()).hexdigest()[:16] + + # Get or create conversation + conversation = await conversation_service.get_or_create_conversation( + chatbot_id=chatbot_id, + user_id=str(user_id), + conversation_id=conv_hash + ) + + # Build conversation history from the request messages (excluding system messages for now) + conversation_history = [] + for msg in request.messages: + if msg.role in ["user", "assistant"]: + conversation_history.append({ + "role": msg.role, + "content": msg.content + }) + + # Get chatbot module and generate response + try: + chatbot_module = module_manager.modules.get("chatbot") + if not chatbot_module: + raise HTTPException(status_code=500, detail="Chatbot module not available") + + # Merge chatbot config with request parameters + effective_config = dict(chatbot.config) + if request.temperature is not None: + effective_config["temperature"] = request.temperature + if request.max_tokens is not None: + effective_config["max_tokens"] = request.max_tokens + + # Use the chatbot module to generate a response + response_data = await chatbot_module.chat( + chatbot_config=effective_config, + message=last_user_message, + conversation_history=conversation_history, + user_id=str(user_id) + ) + + response_content = response_data.get("response", "I'm sorry, I couldn't generate a response.") + + except Exception as e: + # Use fallback response + fallback_responses = chatbot.config.get("fallback_responses", [ + "I'm sorry, I'm having trouble processing your request right now." + ]) + response_content = fallback_responses[0] if fallback_responses else "I'm sorry, I couldn't process your request." + + # Save the conversation messages + for msg in request.messages: + if msg.role == "user": # Only save the new user message + await conversation_service.add_message( + conversation_id=conversation.id, + role=msg.role, + content=msg.content, + metadata={} + ) + + # Save assistant message + assistant_message = await conversation_service.add_message( + conversation_id=conversation.id, + role="assistant", + content=response_content, + metadata={}, + sources=response_data.get("sources") + ) + + # Calculate usage (simple approximation) + prompt_tokens = sum(len(msg.content.split()) for msg in request.messages) + completion_tokens = len(response_content.split()) + total_tokens = prompt_tokens + completion_tokens + + # Create OpenAI-compatible response + response_id = f"chatbot-{chatbot_id}-{int(time.time())}" + + return ChatbotChatCompletionResponse( + id=response_id, + object="chat.completion", + created=int(time.time()), + model=chatbot.config.get("model", "unknown"), + choices=[ + ChatChoice( + index=0, + message=ChatMessage(role="assistant", content=response_content), + finish_reason="stop" + ) + ], + usage=ChatUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens + ) + ) + + except HTTPException: + raise + except Exception as e: + await db.rollback() + log_api_request("chatbot_chat_completions_error", {"error": str(e), "user_id": user_id}) + raise HTTPException(status_code=500, detail=f"Failed to process chat completions: {str(e)}") + + @router.get("/conversations/{chatbot_id}") async def get_chatbot_conversations( chatbot_id: str, @@ -617,6 +801,164 @@ async def external_chat_with_chatbot( raise HTTPException(status_code=500, detail=f"Failed to process chat: {str(e)}") +@router.post("/external/{chatbot_id}/chat/completions", response_model=ChatbotChatCompletionResponse) +async def external_chatbot_chat_completions( + chatbot_id: str, + request: ChatbotChatCompletionRequest, + api_key: APIKey = Depends(get_api_key_auth), + db: AsyncSession = Depends(get_db) +): + """External OpenAI-compatible chat completions endpoint for chatbot with API key authentication""" + log_api_request("external_chatbot_chat_completions", { + "chatbot_id": chatbot_id, + "api_key_id": api_key.id, + "messages_count": len(request.messages) + }) + + try: + # Check if API key can access this chatbot + if not api_key.can_access_chatbot(chatbot_id): + raise HTTPException(status_code=403, detail="API key not authorized for this chatbot") + + # Get the chatbot instance + result = await db.execute( + select(ChatbotInstance) + .where(ChatbotInstance.id == chatbot_id) + ) + chatbot = result.scalar_one_or_none() + + if not chatbot: + raise HTTPException(status_code=404, detail="Chatbot not found") + + if not chatbot.is_active: + raise HTTPException(status_code=400, detail="Chatbot is not active") + + # Find the last user message to extract conversation context + user_messages = [msg for msg in request.messages if msg.role == "user"] + if not user_messages: + raise HTTPException(status_code=400, detail="No user message found in conversation") + + last_user_message = user_messages[-1].content + + # Initialize conversation service + conversation_service = ConversationService(db) + + # For OpenAI format, we'll try to find an existing conversation or create a new one + # We'll use a simple hash of the conversation messages as the conversation identifier + import hashlib + conv_hash = hashlib.md5(str([f"{msg.role}:{msg.content}" for msg in request.messages]).encode()).hexdigest()[:16] + + # Get or create conversation with API key context + conversation = await conversation_service.get_or_create_conversation( + chatbot_id=chatbot_id, + user_id=f"api_key_{api_key.id}", + conversation_id=conv_hash, + title=f"API Chat {datetime.utcnow().strftime('%Y-%m-%d %H:%M')}" + ) + + # Add API key metadata to conversation context if new + if not conversation.context_data.get("api_key_id"): + conversation.context_data = {"api_key_id": api_key.id} + await db.commit() + + # Build conversation history from the request messages + conversation_history = [] + for msg in request.messages: + if msg.role in ["user", "assistant"]: + conversation_history.append({ + "role": msg.role, + "content": msg.content + }) + + # Get chatbot module and generate response + try: + chatbot_module = module_manager.modules.get("chatbot") + if not chatbot_module: + raise HTTPException(status_code=500, detail="Chatbot module not available") + + # Merge chatbot config with request parameters + effective_config = dict(chatbot.config) + if request.temperature is not None: + effective_config["temperature"] = request.temperature + if request.max_tokens is not None: + effective_config["max_tokens"] = request.max_tokens + + # Use the chatbot module to generate a response + response_data = await chatbot_module.chat( + chatbot_config=effective_config, + message=last_user_message, + conversation_history=conversation_history, + user_id=f"api_key_{api_key.id}" + ) + + response_content = response_data.get("response", "I'm sorry, I couldn't generate a response.") + sources = response_data.get("sources") + + except Exception as e: + # Use fallback response + fallback_responses = chatbot.config.get("fallback_responses", [ + "I'm sorry, I'm having trouble processing your request right now." + ]) + response_content = fallback_responses[0] if fallback_responses else "I'm sorry, I couldn't process your request." + sources = None + + # Save the conversation messages + for msg in request.messages: + if msg.role == "user": # Only save the new user message + await conversation_service.add_message( + conversation_id=conversation.id, + role=msg.role, + content=msg.content, + metadata={"api_key_id": api_key.id} + ) + + # Save assistant message using conversation service + assistant_message = await conversation_service.add_message( + conversation_id=conversation.id, + role="assistant", + content=response_content, + metadata={"api_key_id": api_key.id}, + sources=sources + ) + + # Update API key usage stats + prompt_tokens = sum(len(msg.content.split()) for msg in request.messages) + completion_tokens = len(response_content.split()) + total_tokens = prompt_tokens + completion_tokens + + api_key.update_usage(tokens_used=total_tokens, cost_cents=0) + await db.commit() + + # Create OpenAI-compatible response + response_id = f"chatbot-{chatbot_id}-{int(time.time())}" + + return ChatbotChatCompletionResponse( + id=response_id, + object="chat.completion", + created=int(time.time()), + model=chatbot.config.get("model", "unknown"), + choices=[ + ChatChoice( + index=0, + message=ChatMessage(role="assistant", content=response_content), + finish_reason="stop" + ) + ], + usage=ChatUsage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens + ) + ) + + except HTTPException: + raise + except Exception as e: + await db.rollback() + log_api_request("external_chatbot_chat_completions_error", {"error": str(e), "chatbot_id": chatbot_id}) + raise HTTPException(status_code=500, detail=f"Failed to process chat completions: {str(e)}") + + @router.post("/{chatbot_id}/api-key") async def create_chatbot_api_key( chatbot_id: str, @@ -668,7 +1010,7 @@ async def create_chatbot_api_key( "secret_key": full_key, # Only returned on creation "chatbot_id": chatbot_id, "chatbot_name": chatbot.name, - "endpoint": f"/api/v1/chatbot/external/{chatbot_id}/chat", + "endpoint": f"/api/v1/chatbot/external/{chatbot_id}/chat/completions", "scopes": new_api_key.scopes, "rate_limit_per_minute": new_api_key.rate_limit_per_minute, "created_at": new_api_key.created_at.isoformat() diff --git a/backend/app/api/v1/tee.py b/backend/app/api/v1/tee.py deleted file mode 100644 index 7fd3913..0000000 --- a/backend/app/api/v1/tee.py +++ /dev/null @@ -1,334 +0,0 @@ -""" -TEE (Trusted Execution Environment) API endpoints -Handles Privatemode.ai TEE integration endpoints -""" - -import logging -from typing import Dict, Any, Optional - -from fastapi import APIRouter, Depends, HTTPException, Query -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from pydantic import BaseModel, Field - -from app.services.tee_service import tee_service -from app.services.api_key_auth import get_current_api_key_user -from app.models.user import User -from app.models.api_key import APIKey - -logger = logging.getLogger(__name__) - -router = APIRouter(prefix="/tee", tags=["tee"]) -security = HTTPBearer() - - -class AttestationRequest(BaseModel): - """Request model for attestation""" - nonce: Optional[str] = Field(None, description="Optional nonce for attestation") - - -class AttestationVerificationRequest(BaseModel): - """Request model for attestation verification""" - report: str = Field(..., description="Attestation report") - signature: str = Field(..., description="Attestation signature") - certificate_chain: str = Field(..., description="Certificate chain") - nonce: Optional[str] = Field(None, description="Optional nonce") - - -class SecureSessionRequest(BaseModel): - """Request model for secure session creation""" - capabilities: Optional[list] = Field( - default=["confidential_inference", "secure_memory", "attestation"], - description="Requested TEE capabilities" - ) - - -@router.get("/health") -async def get_tee_health(): - """ - Get TEE environment health status - - Returns comprehensive health information about the TEE environment - including capabilities, status, and availability. - """ - try: - health_data = await tee_service.health_check() - return { - "success": True, - "data": health_data - } - except Exception as e: - logger.error(f"TEE health check failed: {e}") - raise HTTPException( - status_code=500, - detail="Failed to get TEE health status" - ) - - -@router.get("/capabilities") -async def get_tee_capabilities( - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Get TEE environment capabilities - - Returns detailed information about TEE capabilities including - supported features, encryption algorithms, and security properties. - - Requires authentication. - """ - try: - user, api_key = current_user - capabilities = await tee_service.get_tee_capabilities() - - return { - "success": True, - "data": capabilities - } - except Exception as e: - logger.error(f"Failed to get TEE capabilities: {e}") - raise HTTPException( - status_code=500, - detail="Failed to get TEE capabilities" - ) - - -@router.post("/attestation") -async def get_attestation( - request: AttestationRequest, - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Get TEE attestation report - - Generates a cryptographic attestation report that proves the integrity - and authenticity of the TEE environment. The report can be used to - verify that code is running in a genuine TEE. - - Requires authentication. - """ - try: - user, api_key = current_user - attestation_data = await tee_service.get_attestation(request.nonce) - - return { - "success": True, - "data": attestation_data - } - except Exception as e: - logger.error(f"Failed to get attestation: {e}") - raise HTTPException( - status_code=500, - detail="Failed to get TEE attestation" - ) - - -@router.post("/attestation/verify") -async def verify_attestation( - request: AttestationVerificationRequest, - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Verify TEE attestation report - - Verifies the authenticity and integrity of a TEE attestation report. - This includes validating the certificate chain, signature, and - measurements against known good values. - - Requires authentication. - """ - try: - user, api_key = current_user - - attestation_data = { - "report": request.report, - "signature": request.signature, - "certificate_chain": request.certificate_chain, - "nonce": request.nonce - } - - verification_result = await tee_service.verify_attestation(attestation_data) - - return { - "success": True, - "data": verification_result - } - except Exception as e: - logger.error(f"Failed to verify attestation: {e}") - raise HTTPException( - status_code=500, - detail="Failed to verify TEE attestation" - ) - - -@router.post("/session") -async def create_secure_session( - request: SecureSessionRequest, - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Create a secure TEE session - - Creates a secure session within the TEE environment with requested - capabilities. The session provides isolated execution context with - enhanced security properties. - - Requires authentication. - """ - try: - user, api_key = current_user - - session_data = await tee_service.create_secure_session( - user_id=str(user.id), - api_key_id=api_key.id - ) - - return { - "success": True, - "data": session_data - } - except Exception as e: - logger.error(f"Failed to create secure session: {e}") - raise HTTPException( - status_code=500, - detail="Failed to create TEE secure session" - ) - - -@router.get("/metrics") -async def get_privacy_metrics( - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Get privacy and security metrics - - Returns comprehensive metrics about TEE usage, privacy protection, - and security status including request counts, data encrypted, - and performance statistics. - - Requires authentication. - """ - try: - user, api_key = current_user - metrics = await tee_service.get_privacy_metrics() - - return { - "success": True, - "data": metrics - } - except Exception as e: - logger.error(f"Failed to get privacy metrics: {e}") - raise HTTPException( - status_code=500, - detail="Failed to get privacy metrics" - ) - - -@router.get("/models") -async def list_tee_models( - current_user: tuple = Depends(get_current_api_key_user) -): - """ - List available TEE models - - Returns a list of AI models available through the TEE environment. - These models provide confidential inference capabilities with - enhanced privacy and security properties. - - Requires authentication. - """ - try: - user, api_key = current_user - models = await tee_service.list_tee_models() - - return { - "success": True, - "data": models, - "count": len(models) - } - except Exception as e: - logger.error(f"Failed to list TEE models: {e}") - raise HTTPException( - status_code=500, - detail="Failed to list TEE models" - ) - - -@router.get("/status") -async def get_tee_status( - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Get comprehensive TEE status - - Returns combined status information including health, capabilities, - and metrics for a complete overview of the TEE environment. - - Requires authentication. - """ - try: - user, api_key = current_user - - # Get all status information - health_data = await tee_service.health_check() - capabilities = await tee_service.get_tee_capabilities() - metrics = await tee_service.get_privacy_metrics() - models = await tee_service.list_tee_models() - - status_data = { - "health": health_data, - "capabilities": capabilities, - "metrics": metrics, - "models": { - "available": len(models), - "list": models - }, - "summary": { - "tee_enabled": health_data.get("tee_enabled", False), - "secure_inference_available": len(models) > 0, - "attestation_available": health_data.get("attestation_available", False), - "privacy_score": metrics.get("privacy_score", 0) - } - } - - return { - "success": True, - "data": status_data - } - except Exception as e: - logger.error(f"Failed to get TEE status: {e}") - raise HTTPException( - status_code=500, - detail="Failed to get TEE status" - ) - - -@router.delete("/cache") -async def clear_attestation_cache( - current_user: tuple = Depends(get_current_api_key_user) -): - """ - Clear attestation cache - - Manually clears the attestation cache to force fresh attestation - reports. This can be useful for debugging or when attestation - requirements change. - - Requires authentication. - """ - try: - user, api_key = current_user - - # Clear the cache - await tee_service.cleanup_expired_cache() - tee_service.attestation_cache.clear() - - return { - "success": True, - "message": "Attestation cache cleared successfully" - } - except Exception as e: - logger.error(f"Failed to clear attestation cache: {e}") - raise HTTPException( - status_code=500, - detail="Failed to clear attestation cache" - ) \ No newline at end of file diff --git a/backend/app/models/api_key.py b/backend/app/models/api_key.py index 1f286f1..453021e 100644 --- a/backend/app/models/api_key.py +++ b/backend/app/models/api_key.py @@ -299,7 +299,8 @@ class APIKey(Base): rate_limit_per_day=144000, allowed_models=[], # Will use chatbot's configured model allowed_endpoints=[ - f"/api/v1/chatbot/external/{chatbot_id}/chat" + f"/api/v1/chatbot/external/{chatbot_id}/chat", + f"/api/v1/chatbot/external/{chatbot_id}/chat/completions" ], allowed_ips=[], allowed_chatbots=[chatbot_id], diff --git a/backend/app/services/tee_service.py b/backend/app/services/tee_service.py deleted file mode 100644 index f584250..0000000 --- a/backend/app/services/tee_service.py +++ /dev/null @@ -1,363 +0,0 @@ -""" -Trusted Execution Environment (TEE) Service -Handles Privatemode.ai TEE integration for confidential computing -""" - -import asyncio -import json -import logging -import os -from typing import Dict, Any, Optional, List -from datetime import datetime, timedelta -from enum import Enum - -import aiohttp -from fastapi import HTTPException, status -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import rsa, padding -from cryptography.hazmat.primitives.serialization import load_pem_public_key -import base64 - -from app.core.config import settings - -logger = logging.getLogger(__name__) - - -class TEEStatus(str, Enum): - """TEE environment status""" - HEALTHY = "healthy" - DEGRADED = "degraded" - OFFLINE = "offline" - UNKNOWN = "unknown" - - -class AttestationStatus(str, Enum): - """Attestation verification status""" - VERIFIED = "verified" - FAILED = "failed" - PENDING = "pending" - EXPIRED = "expired" - - -class TEEService: - """Service for managing Privatemode.ai TEE integration""" - - def __init__(self): - self.privatemode_base_url = "http://privatemode-proxy:8080" - self.privatemode_api_key = settings.PRIVATEMODE_API_KEY - self.session: Optional[aiohttp.ClientSession] = None - self.timeout = aiohttp.ClientTimeout(total=300) # 5 minutes timeout - self.attestation_cache = {} # Cache for attestation results - self.attestation_ttl = timedelta(hours=1) # Cache TTL - - async def _get_session(self) -> aiohttp.ClientSession: - """Get or create aiohttp session""" - if self.session is None or self.session.closed: - self.session = aiohttp.ClientSession( - timeout=self.timeout, - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {self.privatemode_api_key}" - } - ) - return self.session - - async def close(self): - """Close the HTTP session""" - if self.session and not self.session.closed: - await self.session.close() - - async def health_check(self) -> Dict[str, Any]: - """Check TEE environment health""" - try: - session = await self._get_session() - async with session.get(f"{self.privatemode_base_url}/health") as response: - if response.status == 200: - health_data = await response.json() - return { - "status": TEEStatus.HEALTHY.value, - "timestamp": datetime.utcnow().isoformat(), - "tee_enabled": health_data.get("tee_enabled", False), - "attestation_available": health_data.get("attestation_available", False), - "secure_memory": health_data.get("secure_memory", False), - "details": health_data - } - else: - return { - "status": TEEStatus.DEGRADED.value, - "timestamp": datetime.utcnow().isoformat(), - "error": f"HTTP {response.status}" - } - except Exception as e: - logger.error(f"TEE health check error: {e}") - return { - "status": TEEStatus.OFFLINE.value, - "timestamp": datetime.utcnow().isoformat(), - "error": str(e) - } - - async def get_attestation(self, nonce: Optional[str] = None) -> Dict[str, Any]: - """Get TEE attestation report""" - try: - if not nonce: - nonce = base64.b64encode(os.urandom(32)).decode() - - # Check cache first - cache_key = f"attestation_{nonce}" - if cache_key in self.attestation_cache: - cached_result = self.attestation_cache[cache_key] - if datetime.fromisoformat(cached_result["timestamp"]) + self.attestation_ttl > datetime.utcnow(): - return cached_result - - session = await self._get_session() - payload = {"nonce": nonce} - - async with session.post( - f"{self.privatemode_base_url}/attestation", - json=payload - ) as response: - if response.status == 200: - attestation_data = await response.json() - - # Process attestation report - result = { - "status": AttestationStatus.VERIFIED.value, - "timestamp": datetime.utcnow().isoformat(), - "nonce": nonce, - "report": attestation_data.get("report"), - "signature": attestation_data.get("signature"), - "certificate_chain": attestation_data.get("certificate_chain"), - "measurements": attestation_data.get("measurements", {}), - "tee_type": attestation_data.get("tee_type", "unknown"), - "verified": True - } - - # Cache the result - self.attestation_cache[cache_key] = result - - return result - else: - error_text = await response.text() - logger.error(f"TEE attestation failed: {response.status} - {error_text}") - return { - "status": AttestationStatus.FAILED.value, - "timestamp": datetime.utcnow().isoformat(), - "nonce": nonce, - "error": error_text, - "verified": False - } - except Exception as e: - logger.error(f"TEE attestation error: {e}") - return { - "status": AttestationStatus.FAILED.value, - "timestamp": datetime.utcnow().isoformat(), - "nonce": nonce, - "error": str(e), - "verified": False - } - - async def verify_attestation(self, attestation_data: Dict[str, Any]) -> Dict[str, Any]: - """Verify TEE attestation report""" - try: - # Extract components - report = attestation_data.get("report") - signature = attestation_data.get("signature") - cert_chain = attestation_data.get("certificate_chain") - - if not all([report, signature, cert_chain]): - return { - "verified": False, - "status": AttestationStatus.FAILED.value, - "error": "Missing required attestation components" - } - - # Verify signature (simplified - in production, use proper certificate validation) - try: - # This is a placeholder for actual attestation verification - # In production, you would: - # 1. Validate the certificate chain - # 2. Verify the signature using the public key - # 3. Check measurements against known good values - # 4. Validate the nonce - - verification_result = { - "verified": True, - "status": AttestationStatus.VERIFIED.value, - "timestamp": datetime.utcnow().isoformat(), - "certificate_valid": True, - "signature_valid": True, - "measurements_valid": True, - "nonce_valid": True - } - - return verification_result - - except Exception as verify_error: - logger.error(f"Attestation verification failed: {verify_error}") - return { - "verified": False, - "status": AttestationStatus.FAILED.value, - "error": str(verify_error) - } - - except Exception as e: - logger.error(f"Attestation verification error: {e}") - return { - "verified": False, - "status": AttestationStatus.FAILED.value, - "error": str(e) - } - - async def get_tee_capabilities(self) -> Dict[str, Any]: - """Get TEE environment capabilities""" - try: - session = await self._get_session() - async with session.get(f"{self.privatemode_base_url}/capabilities") as response: - if response.status == 200: - capabilities = await response.json() - return { - "timestamp": datetime.utcnow().isoformat(), - "tee_type": capabilities.get("tee_type", "unknown"), - "secure_memory_size": capabilities.get("secure_memory_size", 0), - "encryption_algorithms": capabilities.get("encryption_algorithms", []), - "attestation_types": capabilities.get("attestation_types", []), - "key_management": capabilities.get("key_management", False), - "secure_storage": capabilities.get("secure_storage", False), - "network_isolation": capabilities.get("network_isolation", False), - "confidential_computing": capabilities.get("confidential_computing", False), - "details": capabilities - } - else: - return { - "timestamp": datetime.utcnow().isoformat(), - "error": f"Failed to get capabilities: HTTP {response.status}" - } - except Exception as e: - logger.error(f"TEE capabilities error: {e}") - return { - "timestamp": datetime.utcnow().isoformat(), - "error": str(e) - } - - async def create_secure_session(self, user_id: str, api_key_id: int) -> Dict[str, Any]: - """Create a secure TEE session""" - try: - session = await self._get_session() - payload = { - "user_id": user_id, - "api_key_id": api_key_id, - "timestamp": datetime.utcnow().isoformat(), - "requested_capabilities": [ - "confidential_inference", - "secure_memory", - "attestation" - ] - } - - async with session.post( - f"{self.privatemode_base_url}/session", - json=payload - ) as response: - if response.status == 201: - session_data = await response.json() - return { - "session_id": session_data.get("session_id"), - "status": "active", - "timestamp": datetime.utcnow().isoformat(), - "capabilities": session_data.get("capabilities", []), - "expires_at": session_data.get("expires_at"), - "attestation_token": session_data.get("attestation_token") - } - else: - error_text = await response.text() - logger.error(f"TEE session creation failed: {response.status} - {error_text}") - raise HTTPException( - status_code=response.status, - detail=f"Failed to create TEE session: {error_text}" - ) - except aiohttp.ClientError as e: - logger.error(f"TEE session creation error: {e}") - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="TEE service unavailable" - ) - - async def get_privacy_metrics(self) -> Dict[str, Any]: - """Get privacy and security metrics""" - try: - session = await self._get_session() - async with session.get(f"{self.privatemode_base_url}/metrics") as response: - if response.status == 200: - metrics = await response.json() - return { - "timestamp": datetime.utcnow().isoformat(), - "requests_processed": metrics.get("requests_processed", 0), - "data_encrypted": metrics.get("data_encrypted", 0), - "attestations_verified": metrics.get("attestations_verified", 0), - "secure_sessions": metrics.get("secure_sessions", 0), - "uptime": metrics.get("uptime", 0), - "memory_usage": metrics.get("memory_usage", {}), - "performance": metrics.get("performance", {}), - "privacy_score": metrics.get("privacy_score", 0) - } - else: - return { - "timestamp": datetime.utcnow().isoformat(), - "error": f"Failed to get metrics: HTTP {response.status}" - } - except Exception as e: - logger.error(f"TEE metrics error: {e}") - return { - "timestamp": datetime.utcnow().isoformat(), - "error": str(e) - } - - async def list_tee_models(self) -> List[Dict[str, Any]]: - """List available TEE models""" - try: - session = await self._get_session() - async with session.get(f"{self.privatemode_base_url}/models") as response: - if response.status == 200: - models_data = await response.json() - models = [] - - for model in models_data.get("models", []): - models.append({ - "id": model.get("id"), - "name": model.get("name"), - "type": model.get("type", "chat"), - "provider": "privatemode", - "tee_enabled": True, - "confidential_computing": True, - "secure_inference": True, - "attestation_required": model.get("attestation_required", False), - "max_tokens": model.get("max_tokens", 4096), - "cost_per_token": model.get("cost_per_token", 0.0), - "availability": model.get("availability", "available") - }) - - return models - else: - logger.error(f"Failed to get TEE models: HTTP {response.status}") - return [] - except Exception as e: - logger.error(f"TEE models error: {e}") - return [] - - async def cleanup_expired_cache(self): - """Clean up expired attestation cache entries""" - current_time = datetime.utcnow() - expired_keys = [] - - for key, cached_data in self.attestation_cache.items(): - if datetime.fromisoformat(cached_data["timestamp"]) + self.attestation_ttl <= current_time: - expired_keys.append(key) - - for key in expired_keys: - del self.attestation_cache[key] - - logger.info(f"Cleaned up {len(expired_keys)} expired attestation cache entries") - - -# Global TEE service instance -tee_service = TEEService() \ No newline at end of file diff --git a/frontend/src/app/llm/page.tsx b/frontend/src/app/llm/page.tsx index cda9764..3f8beba 100644 --- a/frontend/src/app/llm/page.tsx +++ b/frontend/src/app/llm/page.tsx @@ -70,6 +70,8 @@ function LLMPageContent() { const [models, setModels] = useState([]) const [loading, setLoading] = useState(true) const [showCreateDialog, setShowCreateDialog] = useState(false) + const [showEditDialog, setShowEditDialog] = useState(false) + const [editingKey, setEditingKey] = useState(null) const [showSecretKeyDialog, setShowSecretKeyDialog] = useState(false) const [newSecretKey, setNewSecretKey] = useState('') const { toast } = useToast() @@ -82,6 +84,13 @@ function LLMPageContent() { description: '' }) + // Edit API Key form state + const [editKey, setEditKey] = useState({ + name: '', + description: '', + is_active: true + }) + useEffect(() => { fetchData() }, []) // eslint-disable-line react-hooks/exhaustive-deps @@ -159,6 +168,39 @@ function LLMPageContent() { } } + const openEditDialog = (apiKey: APIKey) => { + setEditingKey(apiKey) + setEditKey({ + name: apiKey.name, + description: apiKey.description || '', + is_active: apiKey.is_active + }) + setShowEditDialog(true) + } + + const updateAPIKey = async () => { + if (!editingKey) return + + try { + await apiClient.put(`/api-internal/v1/api-keys/${editingKey.id}`, editKey) + + toast({ + title: "Success", + description: "API key updated successfully" + }) + + setShowEditDialog(false) + setEditingKey(null) + fetchData() + } catch (error) { + toast({ + title: "Error", + description: "Failed to update API key", + variant: "destructive" + }) + } + } + const deleteAPIKey = async (keyId: number) => { try { console.log('Deleting API key with ID:', keyId) @@ -219,11 +261,11 @@ function LLMPageContent() { if (typeof window !== 'undefined') { const protocol = window.location.protocol const hostname = window.location.hostname - const port = window.location.hostname === 'localhost' ? '3000' : window.location.port || (protocol === 'https:' ? '443' : '80') + const port = window.location.port || (protocol === 'https:' ? '443' : '80') const portSuffix = (protocol === 'https:' && port === '443') || (protocol === 'http:' && port === '80') ? '' : `:${port}` - return `${protocol}//${hostname}${portSuffix}/v1` + return `${protocol}//${hostname}${portSuffix}/api/v1` } - return 'http://localhost:3000/v1' + return 'http://localhost/api/v1' } const publicApiUrl = getPublicApiUrl() @@ -464,7 +506,7 @@ function LLMPageContent() {
- @@ -536,6 +578,71 @@ function LLMPageContent() { + {/* Edit API Key Dialog */} + + + + Edit API Key + + Update the name, description, and status of your API key. + + +
+
+ + setEditKey(prev => ({ ...prev, name: e.target.value }))} + placeholder="e.g., Frontend Application" + /> +
+ +
+ +