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)
851 lines
33 KiB
Python
851 lines
33 KiB
Python
"""
|
||
公开API端点 - 用户可查看的提供商和模型信息
|
||
不包含敏感信息,普通用户可访问
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections import defaultdict
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, Query, Request
|
||
from sqlalchemy import and_, func, or_
|
||
from sqlalchemy.orm import Session, joinedload, load_only
|
||
|
||
from src.api.base.adapter import ApiAdapter, ApiMode
|
||
from src.api.base.context import ApiRequestContext
|
||
from src.api.base.pipeline import get_pipeline
|
||
from src.config.constants import CacheTTL
|
||
from src.core.logger import logger
|
||
from src.database import get_db
|
||
from src.models.api import (
|
||
ProviderStatsResponse,
|
||
PublicGlobalModelListResponse,
|
||
PublicGlobalModelResponse,
|
||
PublicModelResponse,
|
||
PublicProviderResponse,
|
||
)
|
||
from src.models.database import (
|
||
GlobalModel,
|
||
Model,
|
||
Provider,
|
||
ProviderEndpoint,
|
||
RequestCandidate,
|
||
)
|
||
from src.models.endpoint_models import (
|
||
PublicApiFormatHealthMonitor,
|
||
PublicApiFormatHealthMonitorResponse,
|
||
PublicHealthEvent,
|
||
)
|
||
from src.services.health.endpoint import EndpointHealthService
|
||
from src.services.system.config import SystemConfigService
|
||
from src.utils.cache_decorator import cache_result
|
||
|
||
router = APIRouter(prefix="/api/public", tags=["System Catalog"])
|
||
python_host_router = APIRouter(prefix="/api/public", tags=["System Catalog"])
|
||
pipeline = get_pipeline()
|
||
|
||
|
||
def _fetch_recent_public_health_attempts_for_api_format(
|
||
db: Session,
|
||
*,
|
||
api_format: str,
|
||
since: datetime,
|
||
per_format_limit: int,
|
||
) -> list[RequestCandidate]:
|
||
"""获取单个 API 格式最近的最终态请求,用于公开监控事件列表。"""
|
||
final_statuses = ["success", "failed", "skipped"]
|
||
return (
|
||
db.query(RequestCandidate)
|
||
.join(ProviderEndpoint, RequestCandidate.endpoint_id == ProviderEndpoint.id)
|
||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||
.filter(
|
||
ProviderEndpoint.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
ProviderEndpoint.api_format == api_format,
|
||
RequestCandidate.created_at >= since,
|
||
RequestCandidate.status.in_(final_statuses),
|
||
)
|
||
.order_by(RequestCandidate.created_at.desc())
|
||
.limit(per_format_limit)
|
||
.all()
|
||
)
|
||
|
||
|
||
@router.get("/site-info")
|
||
async def get_site_info(request: Request, db: Session = Depends(get_db)) -> Any:
|
||
"""获取站点基本信息(公开接口,无需认证)"""
|
||
adapter = PublicSiteInfoAdapter()
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
|
||
@router.get("/providers", response_model=list[PublicProviderResponse])
|
||
async def get_public_providers(
|
||
request: Request,
|
||
is_active: bool | None = Query(None, description="过滤活跃状态"),
|
||
skip: int = Query(0, description="跳过记录数"),
|
||
limit: int = Query(100, description="返回记录数限制"),
|
||
db: Session = Depends(get_db),
|
||
) -> Any:
|
||
"""
|
||
获取提供商列表(用户视图)
|
||
|
||
返回系统中可用的提供商列表,包含提供商的基本信息和统计数据。
|
||
默认只返回活跃的提供商。
|
||
|
||
**查询参数**
|
||
- is_active: 可选,过滤活跃状态。None 表示只返回活跃提供商,True 返回活跃,False 返回非活跃
|
||
- skip: 跳过的记录数,用于分页,默认 0
|
||
- limit: 返回记录数限制,默认 100,最大 100
|
||
|
||
**返回字段**
|
||
- id: 提供商唯一标识符
|
||
- name: 提供商名称(英文标识)
|
||
- display_name: 提供商显示名称
|
||
- description: 提供商描述信息
|
||
- is_active: 是否活跃
|
||
- provider_priority: 提供商优先级
|
||
- models_count: 该提供商下的模型总数
|
||
- active_models_count: 该提供商下活跃的模型数
|
||
- endpoints_count: 该提供商下的端点总数
|
||
- active_endpoints_count: 该提供商下活跃的端点数
|
||
"""
|
||
|
||
adapter = PublicProvidersAdapter(is_active=is_active, skip=skip, limit=limit)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
|
||
@router.get("/models", response_model=list[PublicModelResponse])
|
||
async def get_public_models(
|
||
request: Request,
|
||
provider_id: str | None = Query(None, description="提供商ID过滤"),
|
||
is_active: bool | None = Query(None, description="过滤活跃状态"),
|
||
skip: int = Query(0, description="跳过记录数"),
|
||
limit: int = Query(100, description="返回记录数限制"),
|
||
db: Session = Depends(get_db),
|
||
) -> Any:
|
||
"""
|
||
获取模型列表(用户视图)
|
||
|
||
返回系统中可用的模型列表,包含模型的详细信息和定价。
|
||
默认只返回活跃提供商下的活跃模型。
|
||
|
||
**查询参数**
|
||
- provider_id: 可选,按提供商 ID 过滤,只返回该提供商下的模型
|
||
- is_active: 可选,过滤活跃状态(当前未使用,始终返回活跃模型)
|
||
- skip: 跳过的记录数,用于分页,默认 0
|
||
- limit: 返回记录数限制,默认 100,最大 100
|
||
|
||
**返回字段**
|
||
- id: 模型唯一标识符
|
||
- provider_id: 所属提供商 ID
|
||
- provider_name: 提供商名称
|
||
- name: 模型统一名称(优先使用 GlobalModel 名称)
|
||
- display_name: 模型显示名称
|
||
- description: 模型描述信息
|
||
- tags: 模型标签(当前为 null)
|
||
- icon_url: 模型图标 URL
|
||
- input_price_per_1m: 输入价格(每 100 万 token)
|
||
- output_price_per_1m: 输出价格(每 100 万 token)
|
||
- cache_creation_price_per_1m: 缓存创建价格(每 100 万 token)
|
||
- cache_read_price_per_1m: 缓存读取价格(每 100 万 token)
|
||
- supports_vision: 是否支持视觉输入
|
||
- supports_function_calling: 是否支持函数调用
|
||
- supports_streaming: 是否支持流式输出
|
||
- is_active: 是否活跃
|
||
"""
|
||
adapter = PublicModelsAdapter(
|
||
provider_id=provider_id, is_active=is_active, skip=skip, limit=limit
|
||
)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
@router.get("/stats", response_model=ProviderStatsResponse)
|
||
async def get_public_stats(request: Request, db: Session = Depends(get_db)) -> Any:
|
||
"""
|
||
获取系统统计信息
|
||
|
||
返回系统的整体统计数据,包括提供商数量、模型数量和支持的 API 格式。
|
||
只统计活跃的提供商和模型。
|
||
|
||
**返回字段**
|
||
- total_providers: 活跃提供商总数
|
||
- active_providers: 活跃提供商数量(与 total_providers 相同)
|
||
- total_models: 活跃模型总数
|
||
- active_models: 活跃模型数量(与 total_models 相同)
|
||
- supported_formats: 支持的 API 格式列表(如 claude、openai、gemini 等)
|
||
"""
|
||
adapter = PublicStatsAdapter()
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
|
||
@router.get("/search/models")
|
||
async def search_models(
|
||
request: Request,
|
||
q: str = Query(..., description="搜索关键词"),
|
||
provider_id: int | None = Query(None, description="提供商ID过滤"),
|
||
limit: int = Query(20, description="返回记录数限制"),
|
||
db: Session = Depends(get_db),
|
||
) -> Any:
|
||
"""
|
||
搜索模型
|
||
|
||
根据关键词搜索模型,支持按模型名称、显示名称等字段进行模糊匹配。
|
||
只返回活跃提供商下的活跃模型。
|
||
|
||
**查询参数**
|
||
- q: 必填,搜索关键词,支持模糊匹配模型的 provider_model_name、GlobalModel.name 或 GlobalModel.display_name
|
||
- provider_id: 可选,按提供商 ID 过滤,只在该提供商下搜索
|
||
- limit: 返回记录数限制,默认 20,最大值取决于系统配置
|
||
|
||
**返回字段**
|
||
返回符合条件的模型列表,字段与 /api/public/models 接口相同:
|
||
- id: 模型唯一标识符
|
||
- provider_id: 所属提供商 ID
|
||
- provider_name: 提供商名称
|
||
- provider_display_name: 提供商显示名称
|
||
- name: 模型统一名称
|
||
- display_name: 模型显示名称
|
||
- description: 模型描述
|
||
- tags: 模型标签
|
||
- icon_url: 模型图标 URL
|
||
- input_price_per_1m: 输入价格(每 100 万 token)
|
||
- output_price_per_1m: 输出价格(每 100 万 token)
|
||
- cache_creation_price_per_1m: 缓存创建价格(每 100 万 token)
|
||
- cache_read_price_per_1m: 缓存读取价格(每 100 万 token)
|
||
- supports_vision: 是否支持视觉
|
||
- supports_function_calling: 是否支持函数调用
|
||
- supports_streaming: 是否支持流式输出
|
||
- is_active: 是否活跃
|
||
"""
|
||
adapter = PublicSearchModelsAdapter(query=q, provider_id=provider_id, limit=limit)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
@router.get("/health/api-formats", response_model=PublicApiFormatHealthMonitorResponse)
|
||
async def get_public_api_format_health(
|
||
request: Request,
|
||
lookback_hours: int = Query(6, ge=1, le=168, description="回溯小时数"),
|
||
per_format_limit: int = Query(100, ge=10, le=500, description="每个格式的事件数限制"),
|
||
db: Session = Depends(get_db),
|
||
) -> Any:
|
||
"""
|
||
获取各 API 格式的健康监控数据
|
||
|
||
返回系统中各 API 格式(如 Claude、OpenAI、Gemini)的健康状态和历史事件。
|
||
公开版本,不包含敏感信息(如 provider_id、key_id 等)。
|
||
|
||
**查询参数**
|
||
- lookback_hours: 回溯的时间范围(小时),默认 6 小时,范围 1-168(7 天)
|
||
- per_format_limit: 每个 API 格式返回的历史事件数量上限,默认 100,范围 10-500
|
||
|
||
**返回字段**
|
||
- generated_at: 响应生成时间
|
||
- formats: API 格式健康监控数据列表,每个格式包含:
|
||
- api_format: API 格式名称(如 claude、openai、gemini)
|
||
- api_path: 本站入口路径
|
||
- total_attempts: 总请求尝试次数
|
||
- success_count: 成功次数
|
||
- failed_count: 失败次数
|
||
- skipped_count: 跳过次数
|
||
- success_rate: 成功率(success / (success + failed))
|
||
- last_event_at: 最后事件时间
|
||
- events: 历史事件列表,按时间倒序,每个事件包含:
|
||
- timestamp: 事件时间
|
||
- status: 状态(success、failed、skipped)
|
||
- status_code: HTTP 状态码
|
||
- latency_ms: 延迟(毫秒)
|
||
- error_type: 错误类型(如果失败)
|
||
- timeline: 时间线数据,用于展示请求量趋势
|
||
- time_range_start: 时间范围起始
|
||
- time_range_end: 时间范围结束
|
||
"""
|
||
adapter = PublicApiFormatHealthMonitorAdapter(
|
||
lookback_hours=lookback_hours,
|
||
per_format_limit=per_format_limit,
|
||
)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
|
||
@router.get("/global-models", response_model=PublicGlobalModelListResponse)
|
||
async def get_public_global_models(
|
||
request: Request,
|
||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||
limit: int = Query(100, ge=1, le=1000, description="返回记录数限制"),
|
||
is_active: bool | None = Query(None, description="过滤活跃状态"),
|
||
search: str | None = Query(None, description="搜索关键词"),
|
||
db: Session = Depends(get_db),
|
||
) -> Any:
|
||
"""
|
||
获取全局模型(GlobalModel)列表
|
||
|
||
返回系统定义的全局模型列表,用于统一不同提供商的模型标识。
|
||
默认只返回活跃的全局模型。
|
||
|
||
**查询参数**
|
||
- skip: 跳过的记录数,用于分页,默认 0,最小 0
|
||
- limit: 返回记录数限制,默认 100,范围 1-1000
|
||
- is_active: 可选,过滤活跃状态。None 表示只返回活跃模型,True 返回活跃,False 返回非活跃
|
||
- search: 可选,搜索关键词,支持模糊匹配模型名称(name)和显示名称(display_name)
|
||
|
||
**返回字段**
|
||
- models: 全局模型列表,每个模型包含:
|
||
- id: 全局模型唯一标识符(UUID)
|
||
- name: 模型名称(统一标识符)
|
||
- display_name: 模型显示名称
|
||
- is_active: 是否活跃
|
||
- default_price_per_request: 默认的按请求计价配置
|
||
- default_tiered_pricing: 默认的阶梯定价配置
|
||
- supported_capabilities: 支持的能力列表(如 vision、function_calling 等)
|
||
- config: 模型配置信息(如 description、icon_url 等)
|
||
- total: 符合条件的模型总数
|
||
"""
|
||
adapter = PublicGlobalModelsAdapter(
|
||
skip=skip,
|
||
limit=limit,
|
||
is_active=is_active,
|
||
search=search,
|
||
)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
|
||
|
||
|
||
# -------- 公共适配器 --------
|
||
|
||
|
||
class PublicApiAdapter(ApiAdapter):
|
||
mode = ApiMode.PUBLIC
|
||
|
||
def authorize(self, context: ApiRequestContext) -> None: # type: ignore[override]
|
||
return None
|
||
|
||
|
||
class PublicSiteInfoAdapter(PublicApiAdapter):
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
return {
|
||
"site_name": SystemConfigService.get_config(db, "site_name", default="Aether"),
|
||
"site_subtitle": SystemConfigService.get_config(
|
||
db,
|
||
"site_subtitle",
|
||
default="AI Gateway",
|
||
),
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class PublicProvidersAdapter(PublicApiAdapter):
|
||
is_active: bool | None
|
||
skip: int
|
||
limit: int
|
||
|
||
@cache_result(
|
||
key_prefix="public:catalog:providers",
|
||
ttl=CacheTTL.PROVIDER,
|
||
user_specific=False,
|
||
vary_by=["is_active", "skip", "limit"],
|
||
)
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
logger.debug("公共API请求提供商列表")
|
||
query = db.query(Provider).options(
|
||
load_only(
|
||
Provider.id,
|
||
Provider.name,
|
||
Provider.description,
|
||
Provider.is_active,
|
||
Provider.provider_priority,
|
||
)
|
||
)
|
||
if self.is_active is not None:
|
||
query = query.filter(Provider.is_active == self.is_active)
|
||
else:
|
||
query = query.filter(Provider.is_active.is_(True))
|
||
|
||
providers = query.offset(self.skip).limit(self.limit).all()
|
||
provider_ids = [provider.id for provider in providers]
|
||
|
||
models_count_map: dict[str, int] = {}
|
||
active_models_count_map: dict[str, int] = {}
|
||
endpoints_count_map: dict[str, int] = {}
|
||
active_endpoints_count_map: dict[str, int] = {}
|
||
if provider_ids:
|
||
model_counts = (
|
||
db.query(Model.provider_id, func.count(Model.id))
|
||
.filter(Model.provider_id.in_(provider_ids))
|
||
.group_by(Model.provider_id)
|
||
.all()
|
||
)
|
||
models_count_map = {provider_id: int(count) for provider_id, count in model_counts}
|
||
|
||
active_model_counts = (
|
||
db.query(Model.provider_id, func.count(Model.id))
|
||
.filter(Model.provider_id.in_(provider_ids), Model.is_active.is_(True))
|
||
.group_by(Model.provider_id)
|
||
.all()
|
||
)
|
||
active_models_count_map = {
|
||
provider_id: int(count) for provider_id, count in active_model_counts
|
||
}
|
||
|
||
endpoint_counts = (
|
||
db.query(ProviderEndpoint.provider_id, func.count(ProviderEndpoint.id))
|
||
.filter(ProviderEndpoint.provider_id.in_(provider_ids))
|
||
.group_by(ProviderEndpoint.provider_id)
|
||
.all()
|
||
)
|
||
endpoints_count_map = {
|
||
provider_id: int(count) for provider_id, count in endpoint_counts
|
||
}
|
||
|
||
active_endpoint_counts = (
|
||
db.query(ProviderEndpoint.provider_id, func.count(ProviderEndpoint.id))
|
||
.filter(
|
||
ProviderEndpoint.provider_id.in_(provider_ids),
|
||
ProviderEndpoint.is_active.is_(True),
|
||
)
|
||
.group_by(ProviderEndpoint.provider_id)
|
||
.all()
|
||
)
|
||
active_endpoints_count_map = {
|
||
provider_id: int(count) for provider_id, count in active_endpoint_counts
|
||
}
|
||
|
||
result = []
|
||
for provider in providers:
|
||
models_count = models_count_map.get(provider.id, 0)
|
||
active_models_count = active_models_count_map.get(provider.id, 0)
|
||
endpoints_count = endpoints_count_map.get(provider.id, 0)
|
||
active_endpoints_count = active_endpoints_count_map.get(provider.id, 0)
|
||
provider_data = PublicProviderResponse(
|
||
id=provider.id,
|
||
name=provider.name,
|
||
description=provider.description,
|
||
is_active=provider.is_active,
|
||
provider_priority=provider.provider_priority,
|
||
models_count=models_count,
|
||
active_models_count=active_models_count,
|
||
endpoints_count=endpoints_count,
|
||
active_endpoints_count=active_endpoints_count,
|
||
)
|
||
result.append(provider_data.model_dump())
|
||
|
||
logger.debug(f"返回 {len(result)} 个提供商信息")
|
||
return result
|
||
|
||
|
||
@dataclass
|
||
class PublicModelsAdapter(PublicApiAdapter):
|
||
provider_id: str | None
|
||
is_active: bool | None
|
||
skip: int
|
||
limit: int
|
||
|
||
@cache_result(
|
||
key_prefix="public:catalog:models",
|
||
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
|
||
user_specific=False,
|
||
vary_by=["provider_id", "is_active", "skip", "limit"],
|
||
)
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
logger.debug("公共API请求模型列表")
|
||
query = (
|
||
db.query(Model, Provider)
|
||
.options(joinedload(Model.global_model))
|
||
.join(Provider)
|
||
.filter(
|
||
and_(
|
||
Model.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
)
|
||
)
|
||
)
|
||
if self.provider_id is not None:
|
||
query = query.filter(Model.provider_id == self.provider_id)
|
||
results = query.offset(self.skip).limit(self.limit).all()
|
||
|
||
response = []
|
||
for model, provider in results:
|
||
global_model = model.global_model
|
||
display_name = global_model.display_name if global_model else model.provider_model_name
|
||
unified_name = global_model.name if global_model else model.provider_model_name
|
||
model_data = PublicModelResponse(
|
||
id=model.id,
|
||
provider_id=model.provider_id,
|
||
provider_name=provider.name,
|
||
name=unified_name,
|
||
display_name=display_name,
|
||
description=(
|
||
global_model.config.get("description")
|
||
if global_model and global_model.config
|
||
else None
|
||
),
|
||
tags=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(),
|
||
cache_read_price_per_1m=model.get_effective_cache_read_price(),
|
||
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=model.is_active,
|
||
)
|
||
response.append(model_data.model_dump())
|
||
|
||
logger.debug(f"返回 {len(response)} 个模型信息")
|
||
return response
|
||
|
||
|
||
class PublicStatsAdapter(PublicApiAdapter):
|
||
@cache_result(
|
||
key_prefix="public:catalog:stats",
|
||
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
|
||
user_specific=False,
|
||
)
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
logger.debug("公共API请求系统统计信息")
|
||
active_providers = int(
|
||
db.query(func.count(Provider.id)).filter(Provider.is_active.is_(True)).scalar() or 0
|
||
)
|
||
active_models = int(
|
||
db.query(func.count(Model.id))
|
||
.join(Provider)
|
||
.filter(
|
||
and_(
|
||
Model.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
)
|
||
)
|
||
.scalar()
|
||
or 0
|
||
)
|
||
formats = (
|
||
db.query(ProviderEndpoint.api_format)
|
||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||
.filter(
|
||
ProviderEndpoint.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
ProviderEndpoint.api_format.isnot(None),
|
||
)
|
||
.distinct()
|
||
.all()
|
||
)
|
||
supported_formats = [row[0] for row in formats if row[0]]
|
||
stats = ProviderStatsResponse(
|
||
total_providers=active_providers,
|
||
active_providers=active_providers,
|
||
total_models=active_models,
|
||
active_models=active_models,
|
||
supported_formats=supported_formats,
|
||
)
|
||
logger.debug("返回系统统计信息")
|
||
return stats.model_dump()
|
||
|
||
|
||
@dataclass
|
||
class PublicSearchModelsAdapter(PublicApiAdapter):
|
||
query: str
|
||
provider_id: int | None
|
||
limit: int
|
||
|
||
@cache_result(
|
||
key_prefix="public:catalog:search_models",
|
||
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
|
||
user_specific=False,
|
||
vary_by=["query", "provider_id", "limit"],
|
||
)
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
logger.debug(f"公共API搜索模型: {self.query}")
|
||
query_stmt = (
|
||
db.query(Model, Provider)
|
||
.options(joinedload(Model.global_model))
|
||
.join(Provider)
|
||
.outerjoin(GlobalModel, Model.global_model_id == GlobalModel.id)
|
||
.filter(
|
||
and_(
|
||
Model.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
)
|
||
)
|
||
)
|
||
search_filter = (
|
||
Model.provider_model_name.ilike(f"%{self.query}%")
|
||
| GlobalModel.name.ilike(f"%{self.query}%")
|
||
| GlobalModel.display_name.ilike(f"%{self.query}%")
|
||
)
|
||
query_stmt = query_stmt.filter(search_filter)
|
||
if self.provider_id is not None:
|
||
query_stmt = query_stmt.filter(Model.provider_id == self.provider_id)
|
||
results = query_stmt.limit(self.limit).all()
|
||
|
||
response = []
|
||
for model, provider in results:
|
||
global_model = model.global_model
|
||
display_name = global_model.display_name if global_model else model.provider_model_name
|
||
unified_name = global_model.name if global_model else model.provider_model_name
|
||
model_data = PublicModelResponse(
|
||
id=model.id,
|
||
provider_id=model.provider_id,
|
||
provider_name=provider.name,
|
||
name=unified_name,
|
||
display_name=display_name,
|
||
description=(
|
||
global_model.config.get("description")
|
||
if global_model and global_model.config
|
||
else None
|
||
),
|
||
tags=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(),
|
||
cache_read_price_per_1m=model.get_effective_cache_read_price(),
|
||
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=model.is_active,
|
||
)
|
||
response.append(model_data.model_dump())
|
||
|
||
logger.debug(f"搜索 '{self.query}' 返回 {len(response)} 个结果")
|
||
return response
|
||
|
||
|
||
@dataclass
|
||
class PublicApiFormatHealthMonitorAdapter(PublicApiAdapter):
|
||
"""公开版 API 格式健康监控适配器(返回 events 数组,前端复用 EndpointHealthTimeline 组件)"""
|
||
|
||
lookback_hours: int
|
||
per_format_limit: int
|
||
|
||
@cache_result(
|
||
key_prefix="public:catalog:health_api_formats",
|
||
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
|
||
user_specific=False,
|
||
vary_by=["lookback_hours", "per_format_limit"],
|
||
)
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
now = datetime.now(timezone.utc)
|
||
since = now - timedelta(hours=self.lookback_hours)
|
||
|
||
# 1. 获取所有活跃的 API 格式
|
||
active_formats = (
|
||
db.query(ProviderEndpoint.api_format)
|
||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||
.filter(
|
||
ProviderEndpoint.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
)
|
||
.distinct()
|
||
.all()
|
||
)
|
||
|
||
all_formats: list[str] = []
|
||
for (api_format_enum,) in active_formats:
|
||
api_format = (
|
||
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
|
||
)
|
||
all_formats.append(api_format)
|
||
|
||
# API 格式 -> Endpoint ID 映射(用于 Usage 时间线)
|
||
endpoint_rows = (
|
||
db.query(ProviderEndpoint.api_format, ProviderEndpoint.id)
|
||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||
.filter(
|
||
ProviderEndpoint.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
)
|
||
.all()
|
||
)
|
||
endpoint_map: dict[str, list[str]] = defaultdict(list)
|
||
for api_format_enum, endpoint_id in endpoint_rows:
|
||
api_format = (
|
||
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
|
||
)
|
||
endpoint_map[api_format].append(endpoint_id)
|
||
|
||
# 2. 统计窗口内每个 API 格式的真实状态分布
|
||
final_statuses = ["success", "failed", "skipped"]
|
||
status_counts_query = (
|
||
db.query(
|
||
ProviderEndpoint.api_format,
|
||
RequestCandidate.status,
|
||
func.count(RequestCandidate.id).label("count"),
|
||
)
|
||
.join(RequestCandidate, ProviderEndpoint.id == RequestCandidate.endpoint_id)
|
||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||
.filter(
|
||
ProviderEndpoint.is_active.is_(True),
|
||
Provider.is_active.is_(True),
|
||
RequestCandidate.created_at >= since,
|
||
RequestCandidate.status.in_(final_statuses),
|
||
)
|
||
.group_by(ProviderEndpoint.api_format, RequestCandidate.status)
|
||
.all()
|
||
)
|
||
|
||
status_counts: dict[str, dict[str, int]] = {}
|
||
for api_format_enum, status, count in status_counts_query:
|
||
api_format = (
|
||
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
|
||
)
|
||
if api_format not in status_counts:
|
||
status_counts[api_format] = {"success": 0, "failed": 0, "skipped": 0}
|
||
status_counts[api_format][status] = count
|
||
|
||
# 3. 为所有活跃格式生成监控数据
|
||
monitors: list[PublicApiFormatHealthMonitor] = []
|
||
for api_format in all_formats:
|
||
candidates = _fetch_recent_public_health_attempts_for_api_format(
|
||
db=db,
|
||
api_format=api_format,
|
||
since=since,
|
||
per_format_limit=self.per_format_limit,
|
||
)
|
||
|
||
# 统计使用窗口内真实总数,events 仅保留最近样本用于展示。
|
||
format_stats = status_counts.get(api_format, {"success": 0, "failed": 0, "skipped": 0})
|
||
success_count = format_stats.get("success", 0)
|
||
failed_count = format_stats.get("failed", 0)
|
||
skipped_count = format_stats.get("skipped", 0)
|
||
total_attempts = success_count + failed_count + skipped_count
|
||
|
||
# 计算成功率 = success / (success + failed)
|
||
actual_completed = success_count + failed_count
|
||
success_rate = success_count / actual_completed if actual_completed > 0 else 1.0
|
||
|
||
# 转换为公开版事件列表(不含敏感信息如 provider_id, key_id)
|
||
events: list[PublicHealthEvent] = []
|
||
for c in candidates:
|
||
event_time = c.finished_at or c.started_at or c.created_at
|
||
events.append(
|
||
PublicHealthEvent(
|
||
timestamp=event_time,
|
||
status=c.status,
|
||
status_code=c.status_code,
|
||
latency_ms=c.latency_ms,
|
||
error_type=c.error_type,
|
||
)
|
||
)
|
||
|
||
# 最后事件时间
|
||
last_event_at = None
|
||
if candidates:
|
||
last_event_at = (
|
||
candidates[0].finished_at
|
||
or candidates[0].started_at
|
||
or candidates[0].created_at
|
||
)
|
||
|
||
timeline_data = EndpointHealthService._generate_timeline_from_usage(
|
||
db=db,
|
||
endpoint_ids=endpoint_map.get(api_format, []),
|
||
now=now,
|
||
lookback_hours=self.lookback_hours,
|
||
)
|
||
|
||
# 获取本站入口路径
|
||
from src.core.api_format import get_local_path_for_endpoint
|
||
|
||
local_path = get_local_path_for_endpoint(api_format)
|
||
|
||
monitors.append(
|
||
PublicApiFormatHealthMonitor(
|
||
api_format=api_format,
|
||
api_path=local_path,
|
||
total_attempts=total_attempts,
|
||
success_count=success_count,
|
||
failed_count=failed_count,
|
||
skipped_count=skipped_count,
|
||
success_rate=success_rate,
|
||
last_event_at=last_event_at,
|
||
events=events,
|
||
timeline=timeline_data.get("timeline", []),
|
||
time_range_start=timeline_data.get("time_range_start"),
|
||
time_range_end=timeline_data.get("time_range_end"),
|
||
)
|
||
)
|
||
|
||
response = PublicApiFormatHealthMonitorResponse(
|
||
generated_at=now,
|
||
formats=monitors,
|
||
)
|
||
|
||
logger.debug(f"公开健康监控: 返回 {len(monitors)} 个 API 格式的健康数据")
|
||
return response.model_dump()
|
||
|
||
|
||
@dataclass
|
||
class PublicGlobalModelsAdapter(PublicApiAdapter):
|
||
"""公开的 GlobalModel 列表适配器"""
|
||
|
||
skip: int
|
||
limit: int
|
||
is_active: bool | None
|
||
search: str | None
|
||
|
||
@cache_result(
|
||
key_prefix="public:catalog:global_models",
|
||
ttl=CacheTTL.MODEL,
|
||
user_specific=False,
|
||
vary_by=["skip", "limit", "is_active", "search"],
|
||
)
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
logger.debug("公共API请求 GlobalModel 列表")
|
||
|
||
query = db.query(GlobalModel)
|
||
|
||
# 默认只返回活跃的模型
|
||
if self.is_active is not None:
|
||
query = query.filter(GlobalModel.is_active == self.is_active)
|
||
else:
|
||
query = query.filter(GlobalModel.is_active.is_(True))
|
||
|
||
# 搜索过滤
|
||
if self.search:
|
||
search_term = f"%{self.search}%"
|
||
query = query.filter(
|
||
or_(
|
||
GlobalModel.name.ilike(search_term),
|
||
GlobalModel.display_name.ilike(search_term),
|
||
)
|
||
)
|
||
|
||
# 统计总数(避免 Query.count() 生成大子查询)
|
||
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()
|
||
|
||
# 转换为响应格式
|
||
model_responses = []
|
||
for gm in models:
|
||
model_responses.append(
|
||
PublicGlobalModelResponse(
|
||
id=gm.id,
|
||
name=gm.name,
|
||
display_name=gm.display_name,
|
||
is_active=gm.is_active,
|
||
default_price_per_request=gm.default_price_per_request,
|
||
default_tiered_pricing=gm.default_tiered_pricing,
|
||
supported_capabilities=gm.supported_capabilities,
|
||
config=gm.config,
|
||
)
|
||
)
|
||
|
||
logger.debug(f"返回 {len(model_responses)} 个 GlobalModel")
|
||
return PublicGlobalModelListResponse(models=model_responses, total=total).model_dump()
|