Files
wehub-resource-sync 1443d3fdf9
Ruff Format Check / Ruff Format & Lint (push) Failing after 7m39s
Deploy VitePress site to Pages / build (push) Failing after 9m11s
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:26 +08:00

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)}}