mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
672 lines
25 KiB
Python
672 lines
25 KiB
Python
"""
|
||
GlobalModel Admin API
|
||
|
||
提供 GlobalModel 的 CRUD 操作接口
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Body, Depends, Query, Request, Response
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.api.base.admin_adapter import AdminApiAdapter
|
||
from src.api.base.context import ApiRequestContext
|
||
from src.api.base.models_service import invalidate_models_list_cache
|
||
from src.api.base.pipeline import get_pipeline
|
||
from src.core.logger import logger
|
||
from src.database import get_db
|
||
from src.models.pydantic_models import (
|
||
BatchAssignToProvidersRequest,
|
||
BatchAssignToProvidersResponse,
|
||
GlobalModelCreate,
|
||
GlobalModelListResponse,
|
||
GlobalModelProvidersResponse,
|
||
GlobalModelResponse,
|
||
GlobalModelUpdate,
|
||
GlobalModelWithStats,
|
||
ModelCatalogProviderDetail,
|
||
)
|
||
from src.services.model.global_model import GlobalModelService
|
||
|
||
router = APIRouter(prefix="/global", tags=["Admin - Global Models"])
|
||
pipeline = get_pipeline()
|
||
|
||
|
||
@router.get("", response_model=GlobalModelListResponse)
|
||
async def list_global_models(
|
||
request: Request,
|
||
skip: int = Query(0, ge=0),
|
||
limit: int = Query(100, ge=1, le=1000),
|
||
is_active: bool | None = Query(None),
|
||
search: str | None = Query(None),
|
||
db: Session = Depends(get_db),
|
||
) -> GlobalModelListResponse:
|
||
"""
|
||
获取 GlobalModel 列表
|
||
|
||
查询系统中的全局模型列表,支持分页、过滤和搜索功能。
|
||
|
||
**查询参数**:
|
||
- `skip`: 跳过记录数,用于分页(默认 0)
|
||
- `limit`: 返回记录数,用于分页(默认 100,最大 1000)
|
||
- `is_active`: 过滤活跃状态(true/false/null,null 表示不过滤)
|
||
- `search`: 搜索关键词,支持按名称或显示名称模糊搜索
|
||
|
||
**返回字段**:
|
||
- `models`: GlobalModel 列表,每个包含:
|
||
- `id`: GlobalModel ID
|
||
- `name`: 模型名称(唯一)
|
||
- `display_name`: 显示名称
|
||
- `is_active`: 是否活跃
|
||
- `provider_count`: 关联提供商数量
|
||
- 定价和能力配置等其他字段
|
||
- `total`: 返回的模型总数
|
||
"""
|
||
adapter = AdminListGlobalModelsAdapter(
|
||
skip=skip,
|
||
limit=limit,
|
||
is_active=is_active,
|
||
search=search,
|
||
)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.get("/{global_model_id}", response_model=GlobalModelWithStats)
|
||
async def get_global_model(
|
||
request: Request,
|
||
global_model_id: str,
|
||
db: Session = Depends(get_db),
|
||
) -> GlobalModelWithStats:
|
||
"""
|
||
获取单个 GlobalModel 详情
|
||
|
||
查询指定 GlobalModel 的详细信息,包含关联的提供商和价格统计数据。
|
||
|
||
**路径参数**:
|
||
- `global_model_id`: GlobalModel ID
|
||
|
||
**返回字段**:
|
||
- 基础字段:`id`, `name`, `display_name`, `is_active` 等
|
||
- 统计字段:
|
||
- `total_models`: 关联的 Model 实现数量
|
||
- `total_providers`: 关联的提供商数量
|
||
- `price_range`: 价格区间统计(最低/最高输入输出价格)
|
||
"""
|
||
adapter = AdminGetGlobalModelAdapter(global_model_id=global_model_id)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.post("", response_model=GlobalModelResponse, status_code=201)
|
||
async def create_global_model(
|
||
request: Request,
|
||
payload: GlobalModelCreate,
|
||
db: Session = Depends(get_db),
|
||
) -> GlobalModelResponse:
|
||
"""
|
||
创建 GlobalModel
|
||
|
||
创建一个新的全局模型定义,作为多个提供商实现的统一抽象。
|
||
|
||
**请求体字段**:
|
||
- `name`: 模型名称(唯一标识,如 "claude-3-5-sonnet-20241022")
|
||
- `display_name`: 显示名称(如 "Claude 3.5 Sonnet")
|
||
- `is_active`: 是否活跃(默认 true)
|
||
- `default_price_per_request`: 默认按次计费价格(可选)
|
||
- `default_tiered_pricing`: 默认阶梯定价配置(包含多个价格阶梯)
|
||
- `supported_capabilities`: 支持的能力标志(vision、function_calling、streaming)
|
||
- `config`: 额外配置(JSON 格式,如 description、context_window 等)
|
||
|
||
**返回字段**:
|
||
- `id`: 创建的 GlobalModel ID
|
||
- 其他请求体中的所有字段
|
||
"""
|
||
adapter = AdminCreateGlobalModelAdapter(payload=payload)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.patch("/{global_model_id}", response_model=GlobalModelResponse)
|
||
async def update_global_model(
|
||
request: Request,
|
||
global_model_id: str,
|
||
payload: GlobalModelUpdate,
|
||
db: Session = Depends(get_db),
|
||
) -> GlobalModelResponse:
|
||
"""
|
||
更新 GlobalModel
|
||
|
||
更新指定 GlobalModel 的配置信息,支持部分字段更新。
|
||
更新后会自动失效相关缓存。
|
||
|
||
**路径参数**:
|
||
- `global_model_id`: GlobalModel ID
|
||
|
||
**请求体字段**(均为可选):
|
||
- `display_name`: 显示名称
|
||
- `is_active`: 是否活跃
|
||
- `default_price_per_request`: 默认按次计费价格
|
||
- `default_tiered_pricing`: 默认阶梯定价配置
|
||
- `supported_capabilities`: 支持的能力标志
|
||
- `config`: 额外配置
|
||
|
||
**返回字段**:
|
||
- 更新后的完整 GlobalModel 信息
|
||
"""
|
||
adapter = AdminUpdateGlobalModelAdapter(global_model_id=global_model_id, payload=payload)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.delete("/{global_model_id}", status_code=204, response_class=Response)
|
||
async def delete_global_model(
|
||
request: Request,
|
||
global_model_id: str,
|
||
db: Session = Depends(get_db),
|
||
) -> Response:
|
||
"""
|
||
删除 GlobalModel
|
||
|
||
删除指定的 GlobalModel,会级联删除所有关联的 Provider 模型实现。
|
||
删除后会自动失效相关缓存。
|
||
|
||
**路径参数**:
|
||
- `global_model_id`: GlobalModel ID
|
||
|
||
**返回**:
|
||
- 成功删除返回 204 状态码,无响应体
|
||
"""
|
||
adapter = AdminDeleteGlobalModelAdapter(global_model_id=global_model_id)
|
||
await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
return Response(status_code=204)
|
||
|
||
|
||
@router.post("/batch-delete")
|
||
async def batch_delete_global_models(
|
||
request: Request,
|
||
ids: list[str] = Body(..., embed=True, max_length=100),
|
||
db: Session = Depends(get_db),
|
||
) -> dict:
|
||
"""
|
||
批量删除 GlobalModel
|
||
|
||
顺序删除多个 GlobalModel(每个独立提交),避免并行删除导致的锁竞争。
|
||
"""
|
||
adapter = AdminBatchDeleteGlobalModelsAdapter(ids=ids)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.post(
|
||
"/{global_model_id}/assign-to-providers", response_model=BatchAssignToProvidersResponse
|
||
)
|
||
async def batch_assign_to_providers(
|
||
request: Request,
|
||
global_model_id: str,
|
||
payload: BatchAssignToProvidersRequest,
|
||
db: Session = Depends(get_db),
|
||
) -> BatchAssignToProvidersResponse:
|
||
"""
|
||
批量为提供商添加模型实现
|
||
|
||
为指定的 GlobalModel 批量创建多个 Provider 的模型实现(Model 记录)。
|
||
用于快速将一个统一模型分配给多个提供商。
|
||
|
||
**路径参数**:
|
||
- `global_model_id`: GlobalModel ID
|
||
|
||
**请求体字段**:
|
||
- `provider_ids`: 提供商 ID 列表
|
||
- `create_models`: Model 创建配置列表,每个包含:
|
||
- `provider_id`: 提供商 ID
|
||
- `provider_model_name`: 提供商侧的模型名称(如 "claude-3-5-sonnet-20241022")
|
||
- 其他可选字段(价格覆盖、能力覆盖等)
|
||
|
||
**返回字段**:
|
||
- `success`: 成功创建的 Model 列表
|
||
- `errors`: 失败的提供商及错误信息列表
|
||
- `total_requested`: 请求处理的总数
|
||
- `total_success`: 成功创建的数量
|
||
- `total_errors`: 失败的数量
|
||
"""
|
||
adapter = AdminBatchAssignToProvidersAdapter(global_model_id=global_model_id, payload=payload)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.get("/{global_model_id}/providers", response_model=GlobalModelProvidersResponse)
|
||
async def get_global_model_providers(
|
||
request: Request,
|
||
global_model_id: str,
|
||
db: Session = Depends(get_db),
|
||
) -> GlobalModelProvidersResponse:
|
||
"""
|
||
获取 GlobalModel 的关联提供商
|
||
|
||
查询指定 GlobalModel 的所有关联提供商及其模型实现详情,包括非活跃的提供商。
|
||
用于查看某个统一模型在各个提供商上的具体配置。
|
||
|
||
**路径参数**:
|
||
- `global_model_id`: GlobalModel ID
|
||
|
||
**返回字段**:
|
||
- `providers`: 提供商列表,每个包含:
|
||
- `provider_id`: 提供商 ID
|
||
- `provider_name`: 提供商名称
|
||
- `provider_display_name`: 提供商显示名称
|
||
- `model_id`: Model 实现 ID
|
||
- `target_model`: 提供商侧的模型名称
|
||
- 价格信息(input_price_per_1m、output_price_per_1m 等)
|
||
- 能力标志(supports_vision、supports_function_calling、supports_streaming)
|
||
- `is_active`: 是否活跃
|
||
- `total`: 关联提供商总数
|
||
"""
|
||
adapter = AdminGetGlobalModelProvidersAdapter(global_model_id=global_model_id)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
# ========== Adapters ==========
|
||
|
||
|
||
@dataclass
|
||
class AdminListGlobalModelsAdapter(AdminApiAdapter):
|
||
"""列出 GlobalModel"""
|
||
|
||
skip: int
|
||
limit: int
|
||
is_active: bool | None
|
||
search: str | None
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
from sqlalchemy import and_, case, func, or_
|
||
|
||
from src.models.database import GlobalModel, Model, Provider
|
||
|
||
query = context.db.query(GlobalModel)
|
||
if self.is_active is not None:
|
||
query = query.filter(GlobalModel.is_active == self.is_active)
|
||
if self.search:
|
||
search_pattern = f"%{self.search}%"
|
||
query = query.filter(
|
||
or_(
|
||
GlobalModel.name.ilike(search_pattern),
|
||
GlobalModel.display_name.ilike(search_pattern),
|
||
)
|
||
)
|
||
|
||
total = int(query.with_entities(func.count(GlobalModel.id)).scalar() or 0)
|
||
models = query.order_by(GlobalModel.name).offset(self.skip).limit(self.limit).all()
|
||
|
||
# 一次性查询所有 GlobalModel 的 provider_count(优化 N+1 问题)
|
||
# 用条件聚合同时获取总数和活跃数,减少一次 DB 往返
|
||
model_ids = [gm.id for gm in models]
|
||
provider_counts = {}
|
||
active_provider_counts = {}
|
||
if model_ids:
|
||
count_results = (
|
||
context.db.query(
|
||
Model.global_model_id,
|
||
func.count(func.distinct(Model.provider_id)),
|
||
func.count(
|
||
func.distinct(
|
||
case(
|
||
(
|
||
and_(
|
||
Model.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
),
|
||
Model.provider_id,
|
||
),
|
||
else_=None,
|
||
)
|
||
)
|
||
),
|
||
)
|
||
.join(Provider, Model.provider_id == Provider.id)
|
||
.filter(Model.global_model_id.in_(model_ids))
|
||
.group_by(Model.global_model_id)
|
||
.all()
|
||
)
|
||
provider_counts = {gm_id: total for gm_id, total, _ in count_results}
|
||
active_provider_counts = {gm_id: active for gm_id, _, active in count_results}
|
||
|
||
# 构建响应
|
||
model_responses = []
|
||
for gm in models:
|
||
response = GlobalModelResponse.model_validate(gm)
|
||
response.provider_count = provider_counts.get(gm.id, 0)
|
||
response.active_provider_count = active_provider_counts.get(gm.id, 0)
|
||
model_responses.append(response)
|
||
|
||
return GlobalModelListResponse(
|
||
models=model_responses,
|
||
total=total,
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class AdminGetGlobalModelAdapter(AdminApiAdapter):
|
||
"""获取单个 GlobalModel"""
|
||
|
||
global_model_id: str
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
from sqlalchemy import and_, case, func
|
||
|
||
from src.models.database import Model, Provider
|
||
|
||
global_model = GlobalModelService.get_global_model(context.db, self.global_model_id)
|
||
stats = GlobalModelService.get_global_model_stats(context.db, self.global_model_id)
|
||
|
||
# total_providers 已由 stats 提供,这里只查询活跃 provider 数量
|
||
active_count = (
|
||
context.db.query(
|
||
func.count(
|
||
func.distinct(
|
||
case(
|
||
(
|
||
and_(
|
||
Model.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
),
|
||
Model.provider_id,
|
||
),
|
||
else_=None,
|
||
)
|
||
)
|
||
)
|
||
)
|
||
.join(Provider, Model.provider_id == Provider.id)
|
||
.filter(Model.global_model_id == global_model.id)
|
||
.scalar()
|
||
or 0
|
||
)
|
||
|
||
response = GlobalModelResponse.model_validate(global_model)
|
||
response.provider_count = stats["total_providers"]
|
||
response.active_provider_count = int(active_count)
|
||
|
||
return GlobalModelWithStats(
|
||
**response.model_dump(),
|
||
total_models=stats["total_models"],
|
||
total_providers=stats["total_providers"],
|
||
price_range=stats["price_range"],
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class AdminCreateGlobalModelAdapter(AdminApiAdapter):
|
||
"""创建 GlobalModel"""
|
||
|
||
payload: GlobalModelCreate
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
from src.core.exceptions import InvalidRequestException
|
||
from src.core.model_permissions import validate_and_extract_model_mappings
|
||
|
||
# 验证 model_mappings(如果有)
|
||
is_valid, error, _ = validate_and_extract_model_mappings(self.payload.config)
|
||
if not is_valid:
|
||
raise InvalidRequestException(f"映射规则验证失败: {error}", "model_mappings")
|
||
|
||
# 将 TieredPricingConfig 转换为 dict
|
||
tiered_pricing_dict = self.payload.default_tiered_pricing.model_dump()
|
||
|
||
global_model = GlobalModelService.create_global_model(
|
||
db=context.db,
|
||
name=self.payload.name,
|
||
display_name=self.payload.display_name,
|
||
is_active=self.payload.is_active,
|
||
# 按次计费配置
|
||
default_price_per_request=self.payload.default_price_per_request,
|
||
# 阶梯计费配置
|
||
default_tiered_pricing=tiered_pricing_dict,
|
||
# Key 能力配置
|
||
supported_capabilities=self.payload.supported_capabilities,
|
||
# 模型配置(JSON)
|
||
config=self.payload.config,
|
||
)
|
||
|
||
logger.info(f"GlobalModel 已创建: id={global_model.id} name={global_model.name}")
|
||
|
||
# 创建成功后失效缓存(避免 mapping-preview 在 TTL 内读到旧结果)
|
||
from src.services.cache.invalidation import get_cache_invalidation_service
|
||
|
||
cache_service = get_cache_invalidation_service()
|
||
await cache_service.on_global_model_changed(global_model.name, str(global_model.id))
|
||
|
||
return GlobalModelResponse.model_validate(global_model)
|
||
|
||
|
||
@dataclass
|
||
class AdminUpdateGlobalModelAdapter(AdminApiAdapter):
|
||
"""更新 GlobalModel"""
|
||
|
||
global_model_id: str
|
||
payload: GlobalModelUpdate
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
from src.core.exceptions import InvalidRequestException
|
||
from src.core.model_permissions import validate_and_extract_model_mappings
|
||
|
||
# 验证 model_mappings(如果有)
|
||
is_valid, error, _ = validate_and_extract_model_mappings(self.payload.config)
|
||
if not is_valid:
|
||
raise InvalidRequestException(f"映射规则验证失败: {error}", "model_mappings")
|
||
|
||
# 使用行级锁获取旧的 GlobalModel 信息,防止并发更新导致的竞态条件
|
||
# 设置 2 秒锁超时,允许短暂等待而非立即失败,提升并发操作的成功率
|
||
from sqlalchemy import text
|
||
from sqlalchemy.exc import OperationalError
|
||
|
||
from src.models.database import GlobalModel
|
||
|
||
try:
|
||
# 设置会话级别的锁超时(仅影响当前事务)
|
||
context.db.execute(text("SET LOCAL lock_timeout = '2s'"))
|
||
old_global_model = (
|
||
context.db.query(GlobalModel)
|
||
.filter(GlobalModel.id == self.global_model_id)
|
||
.with_for_update()
|
||
.first()
|
||
)
|
||
except OperationalError as e:
|
||
# 锁超时或锁冲突时返回友好的错误提示
|
||
error_msg = str(e).lower()
|
||
if "lock" in error_msg or "timeout" in error_msg:
|
||
raise InvalidRequestException("该模型正在被其他操作更新,请稍后重试")
|
||
raise
|
||
old_model_name = old_global_model.name if old_global_model else None
|
||
|
||
# 执行更新(此时仍持有行锁)
|
||
global_model = GlobalModelService.update_global_model(
|
||
db=context.db,
|
||
global_model_id=self.global_model_id,
|
||
update_data=self.payload,
|
||
)
|
||
|
||
logger.info(f"GlobalModel 已更新: id={global_model.id} name={global_model.name}")
|
||
|
||
# 更新成功后才失效缓存(避免回滚时缓存已被清除的竞态问题)
|
||
# 注意:此时事务已提交(由 pipeline 管理),数据已持久化
|
||
from src.services.cache.invalidation import get_cache_invalidation_service
|
||
|
||
cache_service = get_cache_invalidation_service()
|
||
if old_model_name:
|
||
await cache_service.on_global_model_changed(old_model_name, self.global_model_id)
|
||
|
||
return GlobalModelResponse.model_validate(global_model)
|
||
|
||
|
||
@dataclass
|
||
class AdminDeleteGlobalModelAdapter(AdminApiAdapter):
|
||
"""删除 GlobalModel(级联删除所有关联的 Provider 模型实现)"""
|
||
|
||
global_model_id: str
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
# 使用行级锁获取 GlobalModel 信息,防止并发操作导致的竞态条件
|
||
# 设置 2 秒锁超时,允许短暂等待而非立即失败
|
||
from sqlalchemy import text
|
||
from sqlalchemy.exc import OperationalError
|
||
|
||
from src.core.exceptions import InvalidRequestException
|
||
from src.models.database import GlobalModel
|
||
|
||
try:
|
||
# 设置会话级别的锁超时(仅影响当前事务)
|
||
context.db.execute(text("SET LOCAL lock_timeout = '2s'"))
|
||
global_model = (
|
||
context.db.query(GlobalModel)
|
||
.filter(GlobalModel.id == self.global_model_id)
|
||
.with_for_update()
|
||
.first()
|
||
)
|
||
except OperationalError as e:
|
||
# 锁超时或锁冲突时返回友好的错误提示
|
||
error_msg = str(e).lower()
|
||
if "lock" in error_msg or "timeout" in error_msg:
|
||
raise InvalidRequestException("该模型正在被其他操作处理,请稍后重试")
|
||
raise
|
||
model_name = global_model.name if global_model else None
|
||
model_id = global_model.id if global_model else self.global_model_id
|
||
|
||
# 执行删除(此时仍持有行锁)
|
||
GlobalModelService.delete_global_model(context.db, self.global_model_id)
|
||
|
||
logger.info(f"GlobalModel 已删除: id={self.global_model_id}")
|
||
|
||
# 删除成功后才失效缓存(避免回滚时缓存已被清除的竞态问题)
|
||
from src.services.cache.invalidation import get_cache_invalidation_service
|
||
|
||
cache_service = get_cache_invalidation_service()
|
||
if model_name:
|
||
await cache_service.on_global_model_changed(model_name, model_id)
|
||
|
||
return None
|
||
|
||
|
||
@dataclass
|
||
class AdminBatchDeleteGlobalModelsAdapter(AdminApiAdapter):
|
||
"""批量删除多个 GlobalModel(顺序执行,每个删除独立提交)"""
|
||
|
||
ids: list[str]
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
from src.core.exceptions import NotFoundException
|
||
from src.models.database import GlobalModel
|
||
|
||
success_count = 0
|
||
failed: list[dict] = []
|
||
deleted_names: list[tuple[str, str]] = [] # (name, id)
|
||
|
||
for gm_id in self.ids:
|
||
try:
|
||
gm = context.db.query(GlobalModel).filter(GlobalModel.id == gm_id).first()
|
||
if gm:
|
||
name = gm.name
|
||
mid = gm.id
|
||
GlobalModelService.delete_global_model(context.db, gm_id)
|
||
deleted_names.append((name, mid))
|
||
success_count += 1
|
||
else:
|
||
failed.append({"id": gm_id, "error": "not found"})
|
||
except NotFoundException:
|
||
failed.append({"id": gm_id, "error": "not found"})
|
||
except Exception as e:
|
||
context.db.rollback()
|
||
failed.append({"id": gm_id, "error": str(e)})
|
||
|
||
# 批量失效缓存
|
||
if deleted_names:
|
||
from src.services.cache.invalidation import get_cache_invalidation_service
|
||
|
||
cache_service = get_cache_invalidation_service()
|
||
for name, mid in deleted_names:
|
||
await cache_service.on_global_model_changed(name, mid)
|
||
|
||
logger.info("批量删除 GlobalModel: success={}, failed={}", success_count, len(failed))
|
||
|
||
return {"success_count": success_count, "failed": failed}
|
||
|
||
|
||
@dataclass
|
||
class AdminBatchAssignToProvidersAdapter(AdminApiAdapter):
|
||
"""批量为 Provider 添加 GlobalModel 实现"""
|
||
|
||
global_model_id: str
|
||
payload: BatchAssignToProvidersRequest
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
result = GlobalModelService.batch_assign_to_providers(
|
||
db=context.db,
|
||
global_model_id=self.global_model_id,
|
||
provider_ids=self.payload.provider_ids,
|
||
create_models=self.payload.create_models,
|
||
)
|
||
|
||
# 如果有成功创建的关联,清除 /v1/models 列表缓存
|
||
if result["success"]:
|
||
await invalidate_models_list_cache()
|
||
|
||
logger.info(
|
||
f"批量为 Provider 添加 GlobalModel: global_model_id={self.global_model_id} success={len(result['success'])} errors={len(result['errors'])}"
|
||
)
|
||
|
||
return BatchAssignToProvidersResponse(**result)
|
||
|
||
|
||
@dataclass
|
||
class AdminGetGlobalModelProvidersAdapter(AdminApiAdapter):
|
||
"""获取 GlobalModel 的所有关联提供商(包括非活跃的)"""
|
||
|
||
global_model_id: str
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
from sqlalchemy.orm import joinedload
|
||
|
||
from src.models.database import Model
|
||
|
||
global_model = GlobalModelService.get_global_model(context.db, self.global_model_id)
|
||
|
||
# 获取所有关联的 Model(包括非活跃的)
|
||
models = (
|
||
context.db.query(Model)
|
||
.options(joinedload(Model.provider), joinedload(Model.global_model))
|
||
.filter(Model.global_model_id == global_model.id)
|
||
.all()
|
||
)
|
||
|
||
provider_entries = []
|
||
for model in models:
|
||
provider = model.provider
|
||
if not provider:
|
||
continue
|
||
|
||
effective_tiered = model.get_effective_tiered_pricing()
|
||
tier_count = len(effective_tiered.get("tiers", [])) if effective_tiered else 1
|
||
|
||
provider_entries.append(
|
||
ModelCatalogProviderDetail(
|
||
provider_id=provider.id,
|
||
provider_name=provider.name,
|
||
model_id=model.id,
|
||
target_model=model.provider_model_name,
|
||
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(),
|
||
cache_read_price_per_1m=model.get_effective_cache_read_price(),
|
||
cache_1h_creation_price_per_1m=model.get_effective_1h_cache_creation_price(),
|
||
price_per_request=model.get_effective_price_per_request(),
|
||
effective_tiered_pricing=effective_tiered,
|
||
tier_count=tier_count,
|
||
supports_vision=model.get_effective_supports_vision(),
|
||
supports_function_calling=model.get_effective_supports_function_calling(),
|
||
supports_streaming=model.get_effective_supports_streaming(),
|
||
is_active=bool(model.is_active),
|
||
)
|
||
)
|
||
|
||
return GlobalModelProvidersResponse(
|
||
providers=provider_entries,
|
||
total=len(provider_entries),
|
||
)
|