240 lines
6.7 KiB
Python
240 lines
6.7 KiB
Python
"""
|
|
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
|