255 lines
9.9 KiB
Python
255 lines
9.9 KiB
Python
"""独立模型供应商配置路由。"""
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
|
from yuxi.models.providers.service import (
|
|
check_credential_status,
|
|
create_provider_config,
|
|
delete_provider_config,
|
|
fetch_remote_models,
|
|
get_all_model_providers,
|
|
get_model_provider_by_id,
|
|
test_model_status_by_spec,
|
|
update_provider_config,
|
|
)
|
|
from yuxi.storage.postgres.models_business import User
|
|
from yuxi.storage.postgres.manager import pg_manager
|
|
from yuxi.utils import logger
|
|
|
|
model_providers = APIRouter(prefix="/system/model-providers", tags=["model-providers"])
|
|
|
|
|
|
async def _refresh_model_cache() -> None:
|
|
"""刷新模型缓存(CRUD 操作后调用)。"""
|
|
from yuxi.models.providers.cache import model_cache
|
|
|
|
try:
|
|
async with pg_manager.get_async_session_context() as session:
|
|
providers = await get_all_model_providers(session)
|
|
model_cache.rebuild(providers)
|
|
logger.info(f"Model cache refreshed: {len(model_cache.get_all_specs())} models loaded")
|
|
except Exception as e:
|
|
logger.error(f"Failed to refresh model cache: {e}")
|
|
|
|
|
|
class ModelProviderPayload(BaseModel):
|
|
provider_id: str | None = Field(None, description="供应商稳定标识")
|
|
display_name: str | None = Field(None, description="展示名称")
|
|
provider_type: str | None = Field(None, description="供应商适配类型,默认 openai")
|
|
default_protocol: str | None = Field(None, description="默认协议")
|
|
base_url: str | None = Field(None, description="API 基础 URL")
|
|
embedding_base_url: str | None = Field(None, description="Embedding 模型请求基础 URL")
|
|
rerank_base_url: str | None = Field(None, description="Rerank 模型请求基础 URL")
|
|
models_endpoint: str | None = Field(None, description="聊天/通用模型列表端点")
|
|
embedding_models_endpoint: str | None = Field(None, description="Embedding 模型列表端点")
|
|
rerank_models_endpoint: str | None = Field(None, description="Rerank 模型列表端点")
|
|
api_key_env: str | None = Field(None, description="API Key 环境变量名")
|
|
api_key: str | None = Field(None, description="直接配置的 API Key")
|
|
capabilities: list[str] | None = Field(None, description="支持能力")
|
|
enabled_models: list[dict[str, Any]] | None = Field(None, description="已启用模型配置")
|
|
headers_json: dict[str, Any] | None = Field(None, description="额外请求头")
|
|
extra_json: dict[str, Any] | None = Field(None, description="扩展配置")
|
|
is_enabled: bool | None = Field(None, description="是否启用")
|
|
is_builtin: bool | None = Field(None, description="是否内置")
|
|
|
|
|
|
@model_providers.get("")
|
|
async def list_providers(
|
|
current_user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取独立模型供应商配置列表。"""
|
|
providers = await get_all_model_providers(db)
|
|
data = []
|
|
for p in providers:
|
|
d = p.to_dict()
|
|
d["credential_status"] = check_credential_status(p)
|
|
data.append(d)
|
|
return {"success": True, "data": data}
|
|
|
|
|
|
@model_providers.post("")
|
|
async def create_provider(
|
|
payload: ModelProviderPayload,
|
|
current_user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""创建独立模型供应商配置。"""
|
|
try:
|
|
provider = await create_provider_config(
|
|
db,
|
|
payload.model_dump(exclude_none=True),
|
|
current_user.username,
|
|
)
|
|
await db.commit()
|
|
await _refresh_model_cache()
|
|
return {"success": True, "data": provider.to_dict()}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"创建模型供应商失败: {e}")
|
|
raise HTTPException(status_code=500, detail="创建模型供应商失败")
|
|
|
|
|
|
@model_providers.get("/{provider_id}")
|
|
async def get_provider(
|
|
provider_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取单个独立模型供应商配置。"""
|
|
provider = await get_model_provider_by_id(db, provider_id)
|
|
if provider is None:
|
|
raise HTTPException(status_code=404, detail=f"供应商 {provider_id} 不存在")
|
|
data = provider.to_dict()
|
|
data["credential_status"] = check_credential_status(provider)
|
|
return {"success": True, "data": data}
|
|
|
|
|
|
@model_providers.put("/{provider_id}")
|
|
async def update_provider(
|
|
provider_id: str,
|
|
payload: ModelProviderPayload,
|
|
current_user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""更新独立模型供应商配置。"""
|
|
try:
|
|
# 获取用户显式设置过的字段(即使值为 None),以便正确处理清空操作
|
|
unset_fields = payload.model_fields_set
|
|
data = payload.model_dump(exclude_none=True)
|
|
for nullable_field in (
|
|
"api_key_env",
|
|
"api_key",
|
|
"default_protocol",
|
|
"embedding_base_url",
|
|
"rerank_base_url",
|
|
"models_endpoint",
|
|
"embedding_models_endpoint",
|
|
"rerank_models_endpoint",
|
|
):
|
|
if nullable_field in unset_fields and getattr(payload, nullable_field) is None:
|
|
data[nullable_field] = None
|
|
provider = await update_provider_config(db, provider_id, data, current_user.username)
|
|
if provider is None:
|
|
raise HTTPException(status_code=404, detail=f"供应商 {provider_id} 不存在")
|
|
await db.commit()
|
|
await _refresh_model_cache()
|
|
return {"success": True, "data": provider.to_dict()}
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"更新模型供应商失败 {provider_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="更新模型供应商失败")
|
|
|
|
|
|
@model_providers.delete("/{provider_id}")
|
|
async def delete_provider(
|
|
provider_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""删除独立模型供应商配置。"""
|
|
deleted = await delete_provider_config(db, provider_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail=f"供应商 {provider_id} 不存在")
|
|
await db.commit()
|
|
await _refresh_model_cache()
|
|
return {"success": True}
|
|
|
|
|
|
@model_providers.get("/{provider_id}/remote-models")
|
|
async def get_remote_models(
|
|
provider_id: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""实时拉取远端 /models,不落库。"""
|
|
provider = await get_model_provider_by_id(db, provider_id)
|
|
if provider is None:
|
|
raise HTTPException(status_code=404, detail=f"供应商 {provider_id} 不存在")
|
|
try:
|
|
models = await fetch_remote_models(provider)
|
|
return {"success": True, "data": models}
|
|
except httpx.HTTPStatusError as e:
|
|
# 远程 API 返回的错误,不透传状态码避免前端误判为系统认证失败
|
|
detail = e.response.text
|
|
if e.response.status_code == 401:
|
|
raise HTTPException(status_code=502, detail="远端 API 认证失败,请检查 API Key 配置")
|
|
raise HTTPException(status_code=e.response.status_code, detail=f"Models 请求失败: {detail}")
|
|
except Exception as e:
|
|
logger.error(f"拉取远端模型失败 {provider_id}: {e}")
|
|
raise HTTPException(status_code=400, detail=f"拉取远端模型失败: {e}")
|
|
|
|
|
|
@model_providers.post("/models/cache/refresh")
|
|
async def refresh_model_cache(
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
"""强制刷新模型缓存,从数据库重新加载所有供应商配置到 Redis。"""
|
|
await _refresh_model_cache()
|
|
from yuxi.models.providers.cache import model_cache
|
|
|
|
return {"success": True, "message": "缓存已刷新", "model_count": len(model_cache.get_all_specs())}
|
|
|
|
|
|
@model_providers.get("/models/v2")
|
|
async def get_v2_models(
|
|
model_type: str = "chat",
|
|
_current_user: User = Depends(get_required_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""获取 v2 格式的模型列表,按 provider 分组。
|
|
|
|
v2 模型 spec 格式: provider_id:model_id(冒号分隔)
|
|
返回数据供前端模型选择器使用。
|
|
"""
|
|
from yuxi.models.providers.cache import model_cache
|
|
|
|
grouped = model_cache.get_specs_grouped_by_provider(model_type)
|
|
providers = await get_all_model_providers(db)
|
|
provider_name_by_id = {
|
|
provider.provider_id: provider.display_name or provider.provider_id for provider in providers
|
|
}
|
|
|
|
result = {}
|
|
for provider_id, models in grouped.items():
|
|
result[provider_id] = {
|
|
"provider_id": provider_id,
|
|
"provider_display_name": provider_name_by_id.get(provider_id, provider_id),
|
|
"models": [
|
|
{
|
|
"spec": m.spec,
|
|
"model_id": m.model_id,
|
|
"display_name": m.display_name,
|
|
"dimension": m.dimension,
|
|
"batch_size": m.batch_size,
|
|
}
|
|
for m in models
|
|
]
|
|
}
|
|
|
|
return {"success": True, "data": result}
|
|
|
|
|
|
@model_providers.get("/models/status")
|
|
async def get_model_status_by_spec(
|
|
spec: str,
|
|
current_user: User = Depends(get_admin_user),
|
|
):
|
|
"""根据 full spec 检查模型状态(自动识别 V1/V2、Chat/Embedding)。"""
|
|
try:
|
|
result = await test_model_status_by_spec(spec)
|
|
return {"success": True, "data": result}
|
|
except Exception as e:
|
|
logger.error(f"测试模型状态失败 {spec}: {e}")
|
|
return {"success": False, "data": {"spec": spec, "status": "error", "message": str(e)}}
|