""" LLM Configuration Management API. [AC-ASA-14, AC-ASA-15, AC-ASA-16, AC-ASA-17, AC-ASA-18] LLM provider management endpoints. """ import logging from typing import Any from fastapi import APIRouter, Depends, Header, HTTPException from app.services.llm.factory import ( LLMProviderFactory, get_llm_config_manager, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/admin/llm", tags=["LLM Management"]) def get_tenant_id(x_tenant_id: str = Header(..., alias="X-Tenant-Id")) -> str: """Extract tenant ID from header.""" if not x_tenant_id: raise HTTPException(status_code=400, detail="X-Tenant-Id header is required") return x_tenant_id @router.get("/providers") async def list_providers( tenant_id: str = Depends(get_tenant_id), ) -> dict[str, Any]: """ List all available LLM providers. [AC-ASA-15] Returns provider list with configuration schemas. """ logger.info(f"[AC-ASA-15] Listing LLM providers for tenant={tenant_id}") providers = LLMProviderFactory.get_providers() return { "providers": [ { "name": p.name, "display_name": p.display_name, "description": p.description, "config_schema": p.config_schema, } for p in providers ], } @router.get("/config") async def get_config( tenant_id: str = Depends(get_tenant_id), ) -> dict[str, Any]: """ Get current LLM configuration. [AC-ASA-14] Returns current provider and config. """ logger.info(f"[AC-ASA-14] Getting LLM config for tenant={tenant_id}") manager = get_llm_config_manager() config = manager.get_current_config() masked_config = _mask_secrets(config.get("config", {})) return { "provider": config["provider"], "config": masked_config, } @router.put("/config") async def update_config( body: dict[str, Any], tenant_id: str = Depends(get_tenant_id), ) -> dict[str, Any]: """ Update LLM configuration. [AC-ASA-16] Updates provider and config with validation. """ provider = body.get("provider") config = body.get("config", {}) logger.info(f"[AC-ASA-16] Updating LLM config for tenant={tenant_id}, provider={provider}") if not provider: return { "success": False, "message": "Provider is required", } try: manager = get_llm_config_manager() await manager.update_config(provider, config) return { "success": True, "message": f"LLM configuration updated to {provider}", } except ValueError as e: logger.error(f"[AC-ASA-16] Invalid LLM config: {e}") return { "success": False, "message": str(e), } @router.post("/test") async def test_connection( body: dict[str, Any] | None = None, tenant_id: str = Depends(get_tenant_id), ) -> dict[str, Any]: """ Test LLM connection. [AC-ASA-17, AC-ASA-18] Tests connection and returns response. """ body = body or {} test_prompt = body.get("test_prompt", "你好,请简单介绍一下自己。") provider = body.get("provider") config = body.get("config") logger.info( f"[AC-ASA-17] Testing LLM connection for tenant={tenant_id}, " f"provider={provider or 'current'}" ) manager = get_llm_config_manager() result = await manager.test_connection( test_prompt=test_prompt, provider=provider, config=config, ) return result def _mask_secrets(config: dict[str, Any]) -> dict[str, Any]: """Mask secret fields in config for display.""" masked = {} for key, value in config.items(): if key in ("api_key", "password", "secret"): if value: masked[key] = f"{str(value)[:4]}***" else: masked[key] = "" else: masked[key] = value return masked