253 lines
8.7 KiB
Python
253 lines
8.7 KiB
Python
# [DEF:backend/src/api/routes/llm.py:Module]
|
|
# @TIER: STANDARD
|
|
# @SEMANTICS: api, routes, llm
|
|
# @PURPOSE: API routes for LLM provider configuration and management.
|
|
# @LAYER: UI (API)
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from typing import List, Optional
|
|
from ...core.logger import logger
|
|
from ...schemas.auth import User
|
|
from ...dependencies import get_current_user as get_current_active_user
|
|
from ...plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType
|
|
from ...services.llm_provider import LLMProviderService
|
|
from ...core.database import get_db
|
|
from sqlalchemy.orm import Session
|
|
|
|
# [DEF:router:Global]
|
|
# @PURPOSE: APIRouter instance for LLM routes.
|
|
router = APIRouter(tags=["LLM"])
|
|
# [/DEF:router:Global]
|
|
|
|
|
|
# [DEF:_is_valid_runtime_api_key:Function]
|
|
# @PURPOSE: Validate decrypted runtime API key presence/shape.
|
|
# @PRE: value can be None.
|
|
# @POST: Returns True only for non-placeholder key.
|
|
def _is_valid_runtime_api_key(value: Optional[str]) -> bool:
|
|
key = (value or "").strip()
|
|
if not key:
|
|
return False
|
|
if key in {"********", "EMPTY_OR_NONE"}:
|
|
return False
|
|
return len(key) >= 16
|
|
# [/DEF:_is_valid_runtime_api_key:Function]
|
|
|
|
# [DEF:get_providers:Function]
|
|
# @PURPOSE: Retrieve all LLM provider configurations.
|
|
# @PRE: User is authenticated.
|
|
# @POST: Returns list of LLMProviderConfig.
|
|
@router.get("/providers", response_model=List[LLMProviderConfig])
|
|
async def get_providers(
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Get all LLM provider configurations.
|
|
"""
|
|
logger.info(f"[llm_routes][get_providers][Action] Fetching providers for user: {current_user.username}")
|
|
service = LLMProviderService(db)
|
|
providers = service.get_all_providers()
|
|
return [
|
|
LLMProviderConfig(
|
|
id=p.id,
|
|
provider_type=LLMProviderType(p.provider_type),
|
|
name=p.name,
|
|
base_url=p.base_url,
|
|
api_key="********",
|
|
default_model=p.default_model,
|
|
is_active=p.is_active
|
|
) for p in providers
|
|
]
|
|
# [/DEF:get_providers:Function]
|
|
|
|
|
|
# [DEF:get_llm_status:Function]
|
|
# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation.
|
|
# @PRE: User is authenticated.
|
|
# @POST: configured=true only when an active provider with valid decrypted key exists.
|
|
@router.get("/status")
|
|
async def get_llm_status(
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
service = LLMProviderService(db)
|
|
providers = service.get_all_providers()
|
|
active_provider = next((p for p in providers if p.is_active), None)
|
|
|
|
if not active_provider:
|
|
return {"configured": False, "reason": "no_active_provider"}
|
|
|
|
api_key = service.get_decrypted_api_key(active_provider.id)
|
|
if not _is_valid_runtime_api_key(api_key):
|
|
return {"configured": False, "reason": "invalid_api_key"}
|
|
|
|
return {
|
|
"configured": True,
|
|
"reason": "ok",
|
|
"provider_id": active_provider.id,
|
|
"provider_name": active_provider.name,
|
|
"provider_type": active_provider.provider_type,
|
|
"default_model": active_provider.default_model,
|
|
}
|
|
# [/DEF:get_llm_status:Function]
|
|
|
|
# [DEF:create_provider:Function]
|
|
# @PURPOSE: Create a new LLM provider configuration.
|
|
# @PRE: User is authenticated and has admin permissions.
|
|
# @POST: Returns the created LLMProviderConfig.
|
|
@router.post("/providers", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED)
|
|
async def create_provider(
|
|
config: LLMProviderConfig,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Create a new LLM provider configuration.
|
|
"""
|
|
service = LLMProviderService(db)
|
|
provider = service.create_provider(config)
|
|
return LLMProviderConfig(
|
|
id=provider.id,
|
|
provider_type=LLMProviderType(provider.provider_type),
|
|
name=provider.name,
|
|
base_url=provider.base_url,
|
|
api_key="********",
|
|
default_model=provider.default_model,
|
|
is_active=provider.is_active
|
|
)
|
|
# [/DEF:create_provider:Function]
|
|
|
|
# [DEF:update_provider:Function]
|
|
# @PURPOSE: Update an existing LLM provider configuration.
|
|
# @PRE: User is authenticated and has admin permissions.
|
|
# @POST: Returns the updated LLMProviderConfig.
|
|
@router.put("/providers/{provider_id}", response_model=LLMProviderConfig)
|
|
async def update_provider(
|
|
provider_id: str,
|
|
config: LLMProviderConfig,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Update an existing LLM provider configuration.
|
|
"""
|
|
service = LLMProviderService(db)
|
|
provider = service.update_provider(provider_id, config)
|
|
if not provider:
|
|
raise HTTPException(status_code=404, detail="Provider not found")
|
|
|
|
return LLMProviderConfig(
|
|
id=provider.id,
|
|
provider_type=LLMProviderType(provider.provider_type),
|
|
name=provider.name,
|
|
base_url=provider.base_url,
|
|
api_key="********",
|
|
default_model=provider.default_model,
|
|
is_active=provider.is_active
|
|
)
|
|
# [/DEF:update_provider:Function]
|
|
|
|
# [DEF:delete_provider:Function]
|
|
# @PURPOSE: Delete an LLM provider configuration.
|
|
# @PRE: User is authenticated and has admin permissions.
|
|
# @POST: Returns success status.
|
|
@router.delete("/providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_provider(
|
|
provider_id: str,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Delete an LLM provider configuration.
|
|
"""
|
|
service = LLMProviderService(db)
|
|
if not service.delete_provider(provider_id):
|
|
raise HTTPException(status_code=404, detail="Provider not found")
|
|
return
|
|
# [/DEF:delete_provider:Function]
|
|
|
|
# [DEF:test_connection:Function]
|
|
# @PURPOSE: Test connection to an LLM provider.
|
|
# @PRE: User is authenticated.
|
|
# @POST: Returns success status and message.
|
|
@router.post("/providers/{provider_id}/test")
|
|
async def test_connection(
|
|
provider_id: str,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
logger.info(f"[llm_routes][test_connection][Action] Testing connection for provider_id: {provider_id}")
|
|
"""
|
|
Test connection to an LLM provider.
|
|
"""
|
|
from ...plugins.llm_analysis.service import LLMClient
|
|
service = LLMProviderService(db)
|
|
db_provider = service.get_provider(provider_id)
|
|
if not db_provider:
|
|
raise HTTPException(status_code=404, detail="Provider not found")
|
|
|
|
api_key = service.get_decrypted_api_key(provider_id)
|
|
|
|
# Check if API key was successfully decrypted
|
|
if not api_key:
|
|
logger.error(f"[llm_routes][test_connection] Failed to decrypt API key for provider {provider_id}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Failed to decrypt API key. The provider may have been encrypted with a different encryption key. Please update the provider with a new API key."
|
|
)
|
|
|
|
client = LLMClient(
|
|
provider_type=LLMProviderType(db_provider.provider_type),
|
|
api_key=api_key,
|
|
base_url=db_provider.base_url,
|
|
default_model=db_provider.default_model
|
|
)
|
|
|
|
try:
|
|
# Simple test call
|
|
await client.client.models.list()
|
|
return {"success": True, "message": "Connection successful"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
# [/DEF:test_connection:Function]
|
|
|
|
# [DEF:test_provider_config:Function]
|
|
# @PURPOSE: Test connection with a provided configuration (not yet saved).
|
|
# @PRE: User is authenticated.
|
|
# @POST: Returns success status and message.
|
|
@router.post("/providers/test")
|
|
async def test_provider_config(
|
|
config: LLMProviderConfig,
|
|
current_user: User = Depends(get_current_active_user)
|
|
):
|
|
"""
|
|
Test connection with a provided configuration.
|
|
"""
|
|
from ...plugins.llm_analysis.service import LLMClient
|
|
logger.info(f"[llm_routes][test_provider_config][Action] Testing config for {config.name}")
|
|
|
|
# Check if API key is provided
|
|
if not config.api_key or config.api_key == "********":
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="API key is required for testing connection"
|
|
)
|
|
|
|
client = LLMClient(
|
|
provider_type=config.provider_type,
|
|
api_key=config.api_key,
|
|
base_url=config.base_url,
|
|
default_model=config.default_model
|
|
)
|
|
|
|
try:
|
|
# Simple test call
|
|
await client.client.models.list()
|
|
return {"success": True, "message": "Connection successful"}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
# [/DEF:test_provider_config:Function]
|
|
|
|
# [/DEF:backend/src/api/routes/llm.py]
|