"""Organization settings endpoints.""" from typing import Optional from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel import httpx import base64 from app.core.database import get_db from app.models.organization import Organization, OrganizationMember from app.api.deps import require_role router = APIRouter() class AIConfig(BaseModel): provider: str = "openrouter" apiKey: str = "" model: str = "meta-llama/llama-3.3-70b-instruct" autoAnalyze: bool = True autoCreatePR: bool = True confidenceThreshold: int = 70 class SettingsUpdate(BaseModel): ai_config: Optional[AIConfig] = None class TestLLMRequest(BaseModel): provider: str api_key: str model: str def encrypt_key(key: str) -> str: """Simple obfuscation - in production use proper encryption.""" return base64.b64encode(key.encode()).decode() def decrypt_key(encrypted: str) -> str: """Simple deobfuscation.""" try: return base64.b64decode(encrypted.encode()).decode() except: return "" @router.get("/{org_id}/settings") async def get_settings( org_id: int, member: OrganizationMember = Depends(require_role("viewer")), db: AsyncSession = Depends(get_db) ): """Get organization settings.""" result = await db.execute(select(Organization).where(Organization.id == org_id)) org = result.scalar_one_or_none() if not org: raise HTTPException(status_code=404, detail="Organization not found") return { "ai_config": { "provider": org.ai_provider or "openrouter", "apiKey": "***configured***" if org.ai_api_key_encrypted else "", "model": org.ai_model or "meta-llama/llama-3.3-70b-instruct", "autoAnalyze": org.ai_auto_analyze if org.ai_auto_analyze is not None else True, "autoCreatePR": org.ai_auto_create_pr if org.ai_auto_create_pr is not None else True, "confidenceThreshold": org.ai_confidence_threshold or 70, } } @router.put("/{org_id}/settings") async def update_settings( org_id: int, settings: SettingsUpdate, member: OrganizationMember = Depends(require_role("admin")), db: AsyncSession = Depends(get_db) ): """Update organization settings.""" result = await db.execute(select(Organization).where(Organization.id == org_id)) org = result.scalar_one_or_none() if not org: raise HTTPException(status_code=404, detail="Organization not found") if settings.ai_config: org.ai_provider = settings.ai_config.provider org.ai_model = settings.ai_config.model org.ai_auto_analyze = settings.ai_config.autoAnalyze org.ai_auto_create_pr = settings.ai_config.autoCreatePR org.ai_confidence_threshold = settings.ai_config.confidenceThreshold # Only update key if provided and not masked if settings.ai_config.apiKey and settings.ai_config.apiKey != "***configured***": org.ai_api_key_encrypted = encrypt_key(settings.ai_config.apiKey) await db.commit() return {"message": "Settings updated"} @router.post("/{org_id}/test-llm") async def test_llm_connection( org_id: int, request: TestLLMRequest, member: OrganizationMember = Depends(require_role("admin")), db: AsyncSession = Depends(get_db) ): """Test LLM API connection.""" # Build request based on provider if request.provider == "openrouter": url = "https://openrouter.ai/api/v1/chat/completions" headers = { "Authorization": f"Bearer {request.api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://jira-fixer.startdata.com.br", "X-Title": "JIRA AI Fixer" } payload = { "model": request.model, "messages": [{"role": "user", "content": "Say 'OK' if you can read this."}], "max_tokens": 10 } elif request.provider == "anthropic": url = "https://api.anthropic.com/v1/messages" headers = { "x-api-key": request.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01" } payload = { "model": request.model, "max_tokens": 10, "messages": [{"role": "user", "content": "Say 'OK' if you can read this."}] } elif request.provider == "openai": url = "https://api.openai.com/v1/chat/completions" headers = { "Authorization": f"Bearer {request.api_key}", "Content-Type": "application/json" } payload = { "model": request.model, "messages": [{"role": "user", "content": "Say 'OK' if you can read this."}], "max_tokens": 10 } elif request.provider == "groq": url = "https://api.groq.com/openai/v1/chat/completions" headers = { "Authorization": f"Bearer {request.api_key}", "Content-Type": "application/json" } payload = { "model": request.model, "messages": [{"role": "user", "content": "Say 'OK' if you can read this."}], "max_tokens": 10 } elif request.provider == "google": url = f"https://generativelanguage.googleapis.com/v1beta/models/{request.model}:generateContent?key={request.api_key}" headers = {"Content-Type": "application/json"} payload = { "contents": [{"parts": [{"text": "Say 'OK' if you can read this."}]}], "generationConfig": {"maxOutputTokens": 10} } else: raise HTTPException(status_code=400, detail=f"Unsupported provider: {request.provider}") try: async with httpx.AsyncClient() as client: response = await client.post(url, headers=headers, json=payload, timeout=15.0) if response.status_code == 200: return {"success": True, "message": "Connection successful"} elif response.status_code == 401: raise HTTPException(status_code=400, detail="Invalid API key") elif response.status_code == 403: raise HTTPException(status_code=400, detail="API key lacks permissions") else: error_detail = response.json().get("error", {}).get("message", response.text[:200]) raise HTTPException(status_code=400, detail=f"API error: {error_detail}") except httpx.TimeoutException: raise HTTPException(status_code=400, detail="Connection timeout") except httpx.ConnectError: raise HTTPException(status_code=400, detail="Could not connect to API")