""" 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, LLMUsageType, LLM_USAGE_DISPLAY_NAMES, LLM_USAGE_DESCRIPTIONS, 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("/usage-types") async def list_usage_types( tenant_id: str = Depends(get_tenant_id), ) -> dict[str, Any]: """ List all available LLM usage types. """ logger.info(f"Listing LLM usage types for tenant={tenant_id}") return { "usage_types": [ { "name": ut.value, "display_name": LLM_USAGE_DISPLAY_NAMES[ut], "description": LLM_USAGE_DESCRIPTIONS[ut], } for ut in LLMUsageType ], } @router.get("/config") async def get_config( tenant_id: str = Depends(get_tenant_id), usage_type: str | None = None, ) -> dict[str, Any]: """ Get current LLM configuration. [AC-ASA-14] Returns current provider and config. If usage_type is specified, returns config for that usage type. Otherwise, returns all configs. """ logger.info(f"[AC-ASA-14] Getting LLM config for tenant={tenant_id}, usage_type={usage_type}") manager = get_llm_config_manager() if usage_type: try: ut = LLMUsageType(usage_type) config = manager.get_current_config(ut) masked_config = _mask_secrets(config.get("config", {})) return { "usage_type": config["usage_type"], "provider": config["provider"], "config": masked_config, } except ValueError: raise HTTPException(status_code=400, detail=f"Invalid usage_type: {usage_type}") all_configs = manager.get_current_config() result = {} for ut_key, config in all_configs.items(): result[ut_key] = { "provider": config["provider"], "config": _mask_secrets(config.get("config", {})), } return result @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. Request body format: - For specific usage type: { "usage_type": "chat" | "kb_processing", "provider": "openai", "config": {...} } - For all usage types (backward compatible): { "provider": "openai", "config": {...} } """ provider = body.get("provider") config = body.get("config", {}) usage_type_str = body.get("usage_type") logger.info(f"[AC-ASA-16] Updating LLM config for tenant={tenant_id}, provider={provider}, usage_type={usage_type_str}") if not provider: return { "success": False, "message": "Provider is required", } try: manager = get_llm_config_manager() if usage_type_str: try: usage_type = LLMUsageType(usage_type_str) await manager.update_usage_config(usage_type, provider, config) return { "success": True, "message": f"LLM configuration updated for {usage_type_str} to {provider}", } except ValueError: raise HTTPException(status_code=400, detail=f"Invalid usage_type: {usage_type_str}") else: 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. Request body format: { "test_prompt": "optional test prompt", "provider": "optional provider to test", "config": "optional config to test", "usage_type": "optional usage type to test" } """ body = body or {} test_prompt = body.get("test_prompt", "你好,请简单介绍一下自己。") provider = body.get("provider") config = body.get("config") usage_type_str = body.get("usage_type") logger.info( f"[AC-ASA-17] Testing LLM connection for tenant={tenant_id}, " f"provider={provider or 'current'}, usage_type={usage_type_str or 'default'}" ) manager = get_llm_config_manager() usage_type = None if usage_type_str: try: usage_type = LLMUsageType(usage_type_str) except ValueError: return { "success": False, "error": f"Invalid usage_type: {usage_type_str}", } result = await manager.test_connection( test_prompt=test_prompt, provider=provider, config=config, usage_type=usage_type, ) 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