mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-07 18:22:28 +08:00
refactor(global-model): migrate model metadata to flexible config structure
将模型配置从多个固定字段(description, official_url, icon_url, default_supports_* 等) 统一为灵活的 config JSON 字段,提高扩展性。同时优化前端模型创建表单,支持从 models-dev 列表直接选择模型快速填充。 主要变更: - 后端:模型表迁移,支持 config JSON 存储模型能力和元信息 - 前端:GlobalModelFormDialog 支持两种创建方式(列表选择/手动填写) - API 类型更新,对齐新的数据结构
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .catalog import router as catalog_router
|
||||
from .external import router as external_router
|
||||
from .global_models import router as global_models_router
|
||||
|
||||
router = APIRouter(prefix="/api/admin/models", tags=["Admin - Model Management"])
|
||||
@@ -12,3 +13,4 @@ router = APIRouter(prefix="/api/admin/models", tags=["Admin - Model Management"]
|
||||
# 挂载子路由
|
||||
router.include_router(catalog_router)
|
||||
router.include_router(global_models_router)
|
||||
router.include_router(external_router)
|
||||
|
||||
@@ -72,10 +72,12 @@ class AdminGetModelCatalogAdapter(AdminApiAdapter):
|
||||
for gm in global_models:
|
||||
gm_id = gm.id
|
||||
provider_entries: List[ModelCatalogProviderDetail] = []
|
||||
# 从 config JSON 读取能力标志
|
||||
gm_config = gm.config or {}
|
||||
capability_flags = {
|
||||
"supports_vision": gm.default_supports_vision or False,
|
||||
"supports_function_calling": gm.default_supports_function_calling or False,
|
||||
"supports_streaming": gm.default_supports_streaming or False,
|
||||
"supports_vision": gm_config.get("vision", False),
|
||||
"supports_function_calling": gm_config.get("function_calling", False),
|
||||
"supports_streaming": gm_config.get("streaming", True),
|
||||
}
|
||||
|
||||
# 遍历该 GlobalModel 的所有关联提供商
|
||||
@@ -140,7 +142,7 @@ class AdminGetModelCatalogAdapter(AdminApiAdapter):
|
||||
ModelCatalogItem(
|
||||
global_model_name=gm.name,
|
||||
display_name=gm.display_name,
|
||||
description=gm.description,
|
||||
description=gm_config.get("description"),
|
||||
providers=provider_entries,
|
||||
price_range=price_range,
|
||||
total_providers=len(provider_entries),
|
||||
|
||||
79
src/api/admin/models/external.py
Normal file
79
src/api/admin/models/external.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
models.dev 外部模型数据代理
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.clients import get_redis_client
|
||||
from src.core.logger import logger
|
||||
from src.models.database import User
|
||||
from src.utils.auth_utils import require_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CACHE_KEY = "aether:external:models_dev"
|
||||
CACHE_TTL = 15 * 60 # 15 分钟
|
||||
|
||||
|
||||
async def _get_cached_data() -> Optional[dict[str, Any]]:
|
||||
"""从 Redis 获取缓存数据"""
|
||||
redis = await get_redis_client()
|
||||
if redis is None:
|
||||
return None
|
||||
try:
|
||||
cached = await redis.get(CACHE_KEY)
|
||||
if cached:
|
||||
result: dict[str, Any] = json.loads(cached)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(f"读取 models.dev 缓存失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _set_cached_data(data: dict) -> None:
|
||||
"""将数据写入 Redis 缓存"""
|
||||
redis = await get_redis_client()
|
||||
if redis is None:
|
||||
return
|
||||
try:
|
||||
await redis.setex(CACHE_KEY, CACHE_TTL, json.dumps(data, ensure_ascii=False))
|
||||
except Exception as e:
|
||||
logger.warning(f"写入 models.dev 缓存失败: {e}")
|
||||
|
||||
|
||||
@router.get("/external")
|
||||
async def get_external_models(_: User = Depends(require_admin)) -> JSONResponse:
|
||||
"""
|
||||
获取 models.dev 的模型数据(代理请求,解决跨域问题)
|
||||
数据缓存 15 分钟(使用 Redis,多 worker 共享)
|
||||
"""
|
||||
# 检查缓存
|
||||
cached = await _get_cached_data()
|
||||
if cached is not None:
|
||||
return JSONResponse(content=cached)
|
||||
|
||||
# 从 models.dev 获取数据
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get("https://models.dev/api.json")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# 写入缓存
|
||||
await _set_cached_data(data)
|
||||
|
||||
return JSONResponse(content=data)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(status_code=504, detail="请求 models.dev 超时")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise HTTPException(
|
||||
status_code=502, detail=f"models.dev 返回错误: {e.response.status_code}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"获取外部模型数据失败: {str(e)}")
|
||||
@@ -187,21 +187,15 @@ class AdminCreateGlobalModelAdapter(AdminApiAdapter):
|
||||
db=context.db,
|
||||
name=self.payload.name,
|
||||
display_name=self.payload.display_name,
|
||||
description=self.payload.description,
|
||||
official_url=self.payload.official_url,
|
||||
icon_url=self.payload.icon_url,
|
||||
is_active=self.payload.is_active,
|
||||
# 按次计费配置
|
||||
default_price_per_request=self.payload.default_price_per_request,
|
||||
# 阶梯计费配置
|
||||
default_tiered_pricing=tiered_pricing_dict,
|
||||
# 默认能力配置
|
||||
default_supports_vision=self.payload.default_supports_vision,
|
||||
default_supports_function_calling=self.payload.default_supports_function_calling,
|
||||
default_supports_streaming=self.payload.default_supports_streaming,
|
||||
default_supports_extended_thinking=self.payload.default_supports_extended_thinking,
|
||||
# Key 能力配置
|
||||
supported_capabilities=self.payload.supported_capabilities,
|
||||
# 模型配置(JSON)
|
||||
config=self.payload.config,
|
||||
)
|
||||
|
||||
logger.info(f"GlobalModel 已创建: id={global_model.id} name={global_model.name}")
|
||||
|
||||
@@ -210,9 +210,9 @@ class PublicModelsAdapter(PublicApiAdapter):
|
||||
provider_display_name=provider.display_name,
|
||||
name=unified_name,
|
||||
display_name=display_name,
|
||||
description=global_model.description if global_model else None,
|
||||
description=global_model.config.get("description") if global_model and global_model.config else None,
|
||||
tags=None,
|
||||
icon_url=global_model.icon_url if global_model else None,
|
||||
icon_url=global_model.config.get("icon_url") if global_model and global_model.config else None,
|
||||
input_price_per_1m=model.get_effective_input_price(),
|
||||
output_price_per_1m=model.get_effective_output_price(),
|
||||
cache_creation_price_per_1m=model.get_effective_cache_creation_price(),
|
||||
@@ -274,7 +274,6 @@ class PublicSearchModelsAdapter(PublicApiAdapter):
|
||||
Model.provider_model_name.ilike(f"%{self.query}%")
|
||||
| GlobalModel.name.ilike(f"%{self.query}%")
|
||||
| GlobalModel.display_name.ilike(f"%{self.query}%")
|
||||
| GlobalModel.description.ilike(f"%{self.query}%")
|
||||
)
|
||||
query_stmt = query_stmt.filter(search_filter)
|
||||
if self.provider_id is not None:
|
||||
@@ -293,9 +292,9 @@ class PublicSearchModelsAdapter(PublicApiAdapter):
|
||||
provider_display_name=provider.display_name,
|
||||
name=unified_name,
|
||||
display_name=display_name,
|
||||
description=global_model.description if global_model else None,
|
||||
description=global_model.config.get("description") if global_model and global_model.config else None,
|
||||
tags=None,
|
||||
icon_url=global_model.icon_url if global_model else None,
|
||||
icon_url=global_model.config.get("icon_url") if global_model and global_model.config else None,
|
||||
input_price_per_1m=model.get_effective_input_price(),
|
||||
output_price_per_1m=model.get_effective_output_price(),
|
||||
cache_creation_price_per_1m=model.get_effective_cache_creation_price(),
|
||||
@@ -499,7 +498,6 @@ class PublicGlobalModelsAdapter(PublicApiAdapter):
|
||||
or_(
|
||||
GlobalModel.name.ilike(search_term),
|
||||
GlobalModel.display_name.ilike(search_term),
|
||||
GlobalModel.description.ilike(search_term),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -517,21 +515,11 @@ class PublicGlobalModelsAdapter(PublicApiAdapter):
|
||||
id=gm.id,
|
||||
name=gm.name,
|
||||
display_name=gm.display_name,
|
||||
description=gm.description,
|
||||
icon_url=gm.icon_url,
|
||||
is_active=gm.is_active,
|
||||
default_price_per_request=gm.default_price_per_request,
|
||||
default_tiered_pricing=gm.default_tiered_pricing,
|
||||
default_supports_vision=gm.default_supports_vision or False,
|
||||
default_supports_function_calling=gm.default_supports_function_calling or False,
|
||||
default_supports_streaming=(
|
||||
gm.default_supports_streaming
|
||||
if gm.default_supports_streaming is not None
|
||||
else True
|
||||
),
|
||||
default_supports_extended_thinking=gm.default_supports_extended_thinking
|
||||
or False,
|
||||
supported_capabilities=gm.supported_capabilities,
|
||||
config=gm.config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user