ai-robot-core/ai-service/app/api/admin/llm.py

152 lines
3.9 KiB
Python
Raw Normal View History

"""
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