2026-01-12 23:28:37 +08:00
|
|
|
|
"""
|
|
|
|
|
|
GlobalModel 请求链路预览 API
|
|
|
|
|
|
|
|
|
|
|
|
提供模型的请求链路信息,包括:
|
|
|
|
|
|
- 请求会流向哪些提供商
|
|
|
|
|
|
- 每个提供商的优先级和负载均衡配置
|
|
|
|
|
|
- 模型名称映射关系
|
|
|
|
|
|
- Key 的并发配置和健康状态
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-01-12 23:28:37 +08:00
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
|
|
|
|
|
|
|
|
from src.api.base.admin_adapter import AdminApiAdapter
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from src.api.base.context import ApiRequestContext
|
2026-03-14 11:59:07 +08:00
|
|
|
|
from src.api.base.pipeline import get_pipeline
|
2026-01-15 00:35:28 +08:00
|
|
|
|
from src.core.crypto import CryptoService
|
2026-01-15 09:21:30 +08:00
|
|
|
|
from src.core.model_permissions import (
|
|
|
|
|
|
check_model_allowed_with_mappings,
|
|
|
|
|
|
parse_allowed_models_to_list,
|
|
|
|
|
|
)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
from src.database import get_db
|
|
|
|
|
|
from src.models.database import (
|
|
|
|
|
|
GlobalModel,
|
|
|
|
|
|
Model,
|
2026-01-15 00:35:28 +08:00
|
|
|
|
Provider,
|
2026-01-12 23:28:37 +08:00
|
|
|
|
ProviderAPIKey,
|
|
|
|
|
|
ProviderEndpoint,
|
|
|
|
|
|
)
|
2026-02-16 11:00:48 +08:00
|
|
|
|
from src.services.scheduling.aware_scheduler import CacheAwareScheduler
|
2026-01-12 23:28:37 +08:00
|
|
|
|
from src.services.system.config import SystemConfigService
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/global", tags=["Admin - Global Models"])
|
2026-03-14 11:59:07 +08:00
|
|
|
|
pipeline = get_pipeline()
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== Response Models ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoutingKeyInfo(BaseModel):
|
|
|
|
|
|
"""Key 路由信息"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
masked_key: str = Field("", description="脱敏的 API Key")
|
|
|
|
|
|
internal_priority: int = Field(..., description="Key 内部优先级")
|
2026-02-01 17:28:00 +08:00
|
|
|
|
global_priority_by_format: dict[str, int] | None = Field(
|
|
|
|
|
|
None, description="按 API 格式的全局优先级"
|
|
|
|
|
|
)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
rpm_limit: int | None = Field(None, description="RPM 限制,null 表示自适应")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
is_adaptive: bool = Field(False, description="是否为自适应 RPM 模式")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
effective_rpm: int | None = Field(None, description="有效 RPM 限制")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
cache_ttl_minutes: int = Field(0, description="缓存 TTL(分钟)")
|
2026-01-13 18:42:15 +08:00
|
|
|
|
health_score: float = Field(1.0, description="健康度分数(0-1 小数格式)")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
is_active: bool
|
2026-01-30 03:10:21 +08:00
|
|
|
|
api_formats: list[str] = Field(default_factory=list, description="支持的 API 格式")
|
2026-01-13 16:04:15 +08:00
|
|
|
|
# 模型白名单
|
2026-01-30 03:10:21 +08:00
|
|
|
|
allowed_models: list[str] | None = Field(None, description="允许的模型列表,null 表示不限制")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
# 熔断状态
|
|
|
|
|
|
circuit_breaker_open: bool = Field(False, description="熔断器是否打开")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
circuit_breaker_formats: list[str] = Field(
|
2026-01-15 00:35:28 +08:00
|
|
|
|
default_factory=list, description="熔断的 API 格式列表"
|
|
|
|
|
|
)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
next_probe_at: str | None = Field(None, description="下次探测时间(ISO格式)")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoutingEndpointInfo(BaseModel):
|
|
|
|
|
|
"""Endpoint 路由信息"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
api_format: str
|
|
|
|
|
|
base_url: str
|
2026-01-30 03:10:21 +08:00
|
|
|
|
custom_path: str | None = None
|
2026-01-12 23:28:37 +08:00
|
|
|
|
is_active: bool
|
2026-01-30 03:10:21 +08:00
|
|
|
|
keys: list[RoutingKeyInfo] = Field(default_factory=list)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
total_keys: int = 0
|
|
|
|
|
|
active_keys: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoutingModelMapping(BaseModel):
|
|
|
|
|
|
"""模型名称映射信息"""
|
|
|
|
|
|
|
|
|
|
|
|
name: str = Field(..., description="映射名称")
|
|
|
|
|
|
priority: int = Field(..., description="优先级(数字越小优先级越高)")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
api_formats: list[str] | None = Field(None, description="作用域(适用的 API 格式)")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoutingProviderInfo(BaseModel):
|
|
|
|
|
|
"""Provider 路由信息"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
model_id: str = Field(..., description="Model ID(GlobalModel 与 Provider 的关联记录 ID)")
|
|
|
|
|
|
provider_priority: int = Field(..., description="提供商优先级(数字越小优先级越高)")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
billing_type: str | None = Field(None, description="计费类型")
|
|
|
|
|
|
monthly_quota_usd: float | None = Field(None, description="月额度(美元)")
|
|
|
|
|
|
monthly_used_usd: float | None = Field(None, description="已用额度(美元)")
|
2026-01-12 23:28:37 +08:00
|
|
|
|
is_active: bool
|
|
|
|
|
|
# 模型映射信息
|
|
|
|
|
|
provider_model_name: str = Field(..., description="提供商侧的模型名称")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
model_mappings: list[RoutingModelMapping] = Field(
|
2026-01-12 23:28:37 +08:00
|
|
|
|
default_factory=list, description="模型名称映射列表"
|
|
|
|
|
|
)
|
|
|
|
|
|
model_is_active: bool = Field(True, description="Model 是否活跃")
|
|
|
|
|
|
# Endpoint 和 Key 信息
|
2026-01-30 03:10:21 +08:00
|
|
|
|
endpoints: list[RoutingEndpointInfo] = Field(default_factory=list)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
total_endpoints: int = 0
|
|
|
|
|
|
active_endpoints: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-15 00:35:28 +08:00
|
|
|
|
class GlobalKeyWhitelistItem(BaseModel):
|
|
|
|
|
|
"""全局 Key 白名单项(用于前端实时匹配)"""
|
|
|
|
|
|
|
|
|
|
|
|
key_id: str = Field(..., description="Key ID")
|
|
|
|
|
|
key_name: str = Field(..., description="Key 名称")
|
|
|
|
|
|
masked_key: str = Field(..., description="脱敏的 API Key")
|
|
|
|
|
|
provider_id: str = Field(..., description="Provider ID")
|
|
|
|
|
|
provider_name: str = Field(..., description="Provider 名称")
|
2026-01-30 03:10:21 +08:00
|
|
|
|
allowed_models: list[str] = Field(default_factory=list, description="Key 白名单模型列表")
|
2026-01-15 00:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-12 23:28:37 +08:00
|
|
|
|
class ModelRoutingPreviewResponse(BaseModel):
|
|
|
|
|
|
"""模型请求链路预览响应"""
|
|
|
|
|
|
|
|
|
|
|
|
global_model_id: str
|
|
|
|
|
|
global_model_name: str
|
|
|
|
|
|
display_name: str
|
|
|
|
|
|
is_active: bool
|
2026-01-15 02:36:00 +08:00
|
|
|
|
# GlobalModel 的模型映射(用于前端匹配 Key 白名单)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
global_model_mappings: list[str] = Field(
|
2026-01-15 02:36:00 +08:00
|
|
|
|
default_factory=list, description="GlobalModel 的模型映射规则(正则模式)"
|
|
|
|
|
|
)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
# 链路信息
|
2026-01-30 03:10:21 +08:00
|
|
|
|
providers: list[RoutingProviderInfo] = Field(
|
2026-01-12 23:28:37 +08:00
|
|
|
|
default_factory=list, description="按优先级排序的提供商列表"
|
|
|
|
|
|
)
|
|
|
|
|
|
total_providers: int = 0
|
|
|
|
|
|
active_providers: int = 0
|
|
|
|
|
|
# 调度配置
|
|
|
|
|
|
scheduling_mode: str = Field("cache_affinity", description="调度模式")
|
|
|
|
|
|
priority_mode: str = Field("provider", description="优先级模式")
|
2026-01-15 00:35:28 +08:00
|
|
|
|
# 全局 Key 白名单数据(供前端实时匹配,包含所有 Provider 的 Key)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
all_keys_whitelist: list[GlobalKeyWhitelistItem] = Field(
|
2026-01-15 00:35:28 +08:00
|
|
|
|
default_factory=list, description="所有 Provider 的 Key 白名单数据"
|
|
|
|
|
|
)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== API Endpoints ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{global_model_id}/routing", response_model=ModelRoutingPreviewResponse)
|
|
|
|
|
|
async def get_model_routing_preview(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
global_model_id: str,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> ModelRoutingPreviewResponse:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取模型请求链路预览
|
|
|
|
|
|
|
|
|
|
|
|
查看指定 GlobalModel 的完整请求链路信息,包括:
|
|
|
|
|
|
- 关联的所有提供商及其优先级
|
|
|
|
|
|
- 每个提供商的模型名称映射配置
|
|
|
|
|
|
- Endpoint 和 Key 的详细配置
|
|
|
|
|
|
- 负载均衡和调度策略
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `global_model_id`: GlobalModel ID
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `global_model_id`: GlobalModel ID
|
|
|
|
|
|
- `global_model_name`: 模型名称
|
|
|
|
|
|
- `display_name`: 显示名称
|
|
|
|
|
|
- `is_active`: 是否活跃
|
|
|
|
|
|
- `providers`: 按优先级排序的提供商列表,每个包含:
|
|
|
|
|
|
- `id`: Provider ID
|
|
|
|
|
|
- `name`: Provider 名称
|
|
|
|
|
|
- `provider_priority`: 提供商优先级
|
|
|
|
|
|
- `provider_model_name`: 提供商侧的模型名称
|
|
|
|
|
|
- `model_mappings`: 模型名称映射列表
|
|
|
|
|
|
- `endpoints`: Endpoint 列表,每个包含 Key 信息
|
|
|
|
|
|
- `scheduling_mode`: 调度模式(cache_affinity, fixed_order, load_balance)
|
|
|
|
|
|
- `priority_mode`: 优先级模式(provider, global_key)
|
|
|
|
|
|
"""
|
|
|
|
|
|
adapter = AdminGetModelRoutingPreviewAdapter(global_model_id=global_model_id)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ========== Adapters ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
|
|
|
|
|
|
"""获取模型请求链路预览"""
|
|
|
|
|
|
|
|
|
|
|
|
global_model_id: str
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> ModelRoutingPreviewResponse: # type: ignore[override]
|
2026-01-12 23:28:37 +08:00
|
|
|
|
db = context.db
|
|
|
|
|
|
|
|
|
|
|
|
# 获取 GlobalModel
|
2026-01-15 00:35:28 +08:00
|
|
|
|
global_model = db.query(GlobalModel).filter(GlobalModel.id == self.global_model_id).first()
|
2026-01-12 23:28:37 +08:00
|
|
|
|
if not global_model:
|
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="GlobalModel not found")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取所有关联的 Model(包含 Provider 信息)
|
|
|
|
|
|
models = (
|
|
|
|
|
|
db.query(Model)
|
|
|
|
|
|
.options(selectinload(Model.provider))
|
|
|
|
|
|
.filter(Model.global_model_id == global_model.id)
|
|
|
|
|
|
.all()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取所有相关的 Provider ID
|
|
|
|
|
|
provider_ids = [m.provider_id for m in models if m.provider_id]
|
|
|
|
|
|
|
|
|
|
|
|
# 批量获取 Provider 的 Endpoints
|
2026-01-30 03:10:21 +08:00
|
|
|
|
endpoints_by_provider: dict[str, list[ProviderEndpoint]] = {}
|
2026-01-12 23:28:37 +08:00
|
|
|
|
if provider_ids:
|
|
|
|
|
|
endpoints = (
|
|
|
|
|
|
db.query(ProviderEndpoint)
|
|
|
|
|
|
.filter(ProviderEndpoint.provider_id.in_(provider_ids))
|
|
|
|
|
|
.all()
|
|
|
|
|
|
)
|
|
|
|
|
|
for ep in endpoints:
|
|
|
|
|
|
if ep.provider_id not in endpoints_by_provider:
|
|
|
|
|
|
endpoints_by_provider[ep.provider_id] = []
|
|
|
|
|
|
endpoints_by_provider[ep.provider_id].append(ep)
|
|
|
|
|
|
|
|
|
|
|
|
# 批量获取 Provider 的 Keys
|
2026-01-30 03:10:21 +08:00
|
|
|
|
keys_by_provider: dict[str, list[ProviderAPIKey]] = {}
|
2026-01-12 23:28:37 +08:00
|
|
|
|
if provider_ids:
|
|
|
|
|
|
keys = (
|
2026-01-15 00:35:28 +08:00
|
|
|
|
db.query(ProviderAPIKey).filter(ProviderAPIKey.provider_id.in_(provider_ids)).all()
|
2026-01-12 23:28:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
|
if key.provider_id not in keys_by_provider:
|
|
|
|
|
|
keys_by_provider[key.provider_id] = []
|
|
|
|
|
|
keys_by_provider[key.provider_id].append(key)
|
|
|
|
|
|
|
2026-01-15 09:21:30 +08:00
|
|
|
|
# 提取 GlobalModel 的 model_mappings(用于 Key 白名单匹配)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
global_model_mappings: list[str] = []
|
2026-01-15 09:21:30 +08:00
|
|
|
|
if global_model.config and isinstance(global_model.config, dict):
|
|
|
|
|
|
mappings = global_model.config.get("model_mappings")
|
|
|
|
|
|
if isinstance(mappings, list):
|
|
|
|
|
|
global_model_mappings = [m for m in mappings if isinstance(m, str)]
|
|
|
|
|
|
|
2026-01-12 23:28:37 +08:00
|
|
|
|
# 构建 Provider 路由信息
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_infos: list[RoutingProviderInfo] = []
|
2026-01-12 23:28:37 +08:00
|
|
|
|
for model in models:
|
|
|
|
|
|
provider = model.provider
|
|
|
|
|
|
if not provider:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 获取模型映射
|
|
|
|
|
|
model_mappings = []
|
|
|
|
|
|
if model.provider_model_mappings:
|
|
|
|
|
|
for mapping in model.provider_model_mappings:
|
|
|
|
|
|
model_mappings.append(
|
|
|
|
|
|
RoutingModelMapping(
|
|
|
|
|
|
name=mapping.get("name", ""),
|
|
|
|
|
|
priority=mapping.get("priority", 0),
|
|
|
|
|
|
api_formats=mapping.get("api_formats"),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取 Endpoints
|
|
|
|
|
|
provider_endpoints = endpoints_by_provider.get(provider.id, [])
|
|
|
|
|
|
provider_keys = keys_by_provider.get(provider.id, [])
|
|
|
|
|
|
|
|
|
|
|
|
# 按 api_format 组织 Keys
|
2026-01-30 03:10:21 +08:00
|
|
|
|
keys_by_endpoint: dict[str, list[ProviderAPIKey]] = {}
|
2026-01-12 23:28:37 +08:00
|
|
|
|
for key in provider_keys:
|
|
|
|
|
|
# 每个 Key 可能支持多个 api_formats
|
|
|
|
|
|
for fmt in key.api_formats or []:
|
|
|
|
|
|
if fmt not in keys_by_endpoint:
|
|
|
|
|
|
keys_by_endpoint[fmt] = []
|
|
|
|
|
|
keys_by_endpoint[fmt].append(key)
|
|
|
|
|
|
|
2026-01-22 10:32:06 +08:00
|
|
|
|
# 定义 Key 模型权限匹配检查函数(用于过滤 Key)
|
|
|
|
|
|
def is_key_model_allowed(key: ProviderAPIKey) -> bool:
|
|
|
|
|
|
"""检查 Key 的白名单是否匹配当前 GlobalModel"""
|
|
|
|
|
|
raw_allowed_models = key.allowed_models
|
|
|
|
|
|
if not raw_allowed_models:
|
|
|
|
|
|
# 没有白名单限制,允许所有模型
|
|
|
|
|
|
return True
|
|
|
|
|
|
allowed_models_list = parse_allowed_models_to_list(raw_allowed_models)
|
|
|
|
|
|
is_allowed, _ = check_model_allowed_with_mappings(
|
|
|
|
|
|
model_name=global_model.name,
|
|
|
|
|
|
allowed_models=allowed_models_list,
|
|
|
|
|
|
model_mappings=global_model_mappings,
|
|
|
|
|
|
)
|
|
|
|
|
|
return is_allowed
|
|
|
|
|
|
|
2026-01-12 23:28:37 +08:00
|
|
|
|
endpoint_infos = []
|
|
|
|
|
|
for ep in provider_endpoints:
|
|
|
|
|
|
# 获取该 Endpoint 格式对应的 Keys
|
|
|
|
|
|
ep_keys = keys_by_endpoint.get(ep.api_format or "", [])
|
2026-01-22 10:32:06 +08:00
|
|
|
|
|
|
|
|
|
|
# 过滤:只保留白名单匹配当前 GlobalModel 的 Keys
|
|
|
|
|
|
ep_keys = [k for k in ep_keys if is_key_model_allowed(k)]
|
|
|
|
|
|
|
|
|
|
|
|
# 如果该 Endpoint 没有任何匹配的 Key,跳过此 Endpoint
|
|
|
|
|
|
if not ep_keys:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-01-16 17:53:27 +08:00
|
|
|
|
# 按优先级排序(使用当前格式的全局优先级)
|
|
|
|
|
|
api_format = ep.api_format or ""
|
2026-02-01 17:28:00 +08:00
|
|
|
|
|
2026-01-16 17:53:27 +08:00
|
|
|
|
def get_key_priority(k: ProviderAPIKey) -> tuple[int, int]:
|
|
|
|
|
|
format_priority = 999
|
|
|
|
|
|
if k.global_priority_by_format and api_format in k.global_priority_by_format:
|
|
|
|
|
|
format_priority = k.global_priority_by_format[api_format]
|
|
|
|
|
|
return (format_priority, k.internal_priority or 0)
|
2026-02-01 17:28:00 +08:00
|
|
|
|
|
2026-01-16 17:53:27 +08:00
|
|
|
|
ep_keys.sort(key=get_key_priority)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
key_infos = []
|
|
|
|
|
|
for key in ep_keys:
|
|
|
|
|
|
# 计算有效 RPM
|
|
|
|
|
|
effective_rpm = key.rpm_limit
|
|
|
|
|
|
is_adaptive = key.rpm_limit is None
|
|
|
|
|
|
if is_adaptive and key.learned_rpm_limit:
|
|
|
|
|
|
effective_rpm = key.learned_rpm_limit
|
|
|
|
|
|
|
2026-01-13 18:42:15 +08:00
|
|
|
|
# 从 health_by_format 获取健康度(0-1 小数格式)
|
|
|
|
|
|
health_score = 1.0
|
2026-01-12 23:28:37 +08:00
|
|
|
|
if key.health_by_format and ep.api_format:
|
|
|
|
|
|
format_health = key.health_by_format.get(ep.api_format, {})
|
2026-01-13 18:42:15 +08:00
|
|
|
|
health_score = format_health.get("health_score", 1.0)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
# 生成脱敏 SK(先解密再脱敏)
|
|
|
|
|
|
masked_key = ""
|
|
|
|
|
|
if key.api_key:
|
|
|
|
|
|
crypto = CryptoService()
|
|
|
|
|
|
try:
|
|
|
|
|
|
decrypted_key = crypto.decrypt(key.api_key, silent=True)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# 解密失败时使用加密后的值(可能是未加密的旧数据)
|
|
|
|
|
|
decrypted_key = key.api_key
|
|
|
|
|
|
if len(decrypted_key) > 8:
|
|
|
|
|
|
masked_key = f"{decrypted_key[:4]}***{decrypted_key[-4:]}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
masked_key = f"{decrypted_key[:2]}***"
|
|
|
|
|
|
|
|
|
|
|
|
# 检查熔断状态
|
|
|
|
|
|
circuit_breaker_open = False
|
2026-01-30 03:10:21 +08:00
|
|
|
|
circuit_breaker_formats: list[str] = []
|
|
|
|
|
|
next_probe_at: str | None = None
|
2026-01-12 23:28:37 +08:00
|
|
|
|
if key.circuit_breaker_by_format:
|
|
|
|
|
|
for fmt, cb_state in key.circuit_breaker_by_format.items():
|
|
|
|
|
|
if isinstance(cb_state, dict) and cb_state.get("open"):
|
|
|
|
|
|
circuit_breaker_open = True
|
|
|
|
|
|
circuit_breaker_formats.append(fmt)
|
2026-01-13 18:42:15 +08:00
|
|
|
|
# 取最早的探测时间
|
|
|
|
|
|
fmt_next_probe = cb_state.get("next_probe_at")
|
|
|
|
|
|
if fmt_next_probe:
|
|
|
|
|
|
if next_probe_at is None or fmt_next_probe < next_probe_at:
|
|
|
|
|
|
next_probe_at = fmt_next_probe
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
2026-01-13 16:04:15 +08:00
|
|
|
|
# 解析 allowed_models
|
|
|
|
|
|
raw_allowed_models = key.allowed_models
|
2026-01-14 17:57:21 +08:00
|
|
|
|
allowed_models_list = (
|
|
|
|
|
|
parse_allowed_models_to_list(raw_allowed_models)
|
|
|
|
|
|
if raw_allowed_models
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
2026-01-13 16:04:15 +08:00
|
|
|
|
|
2026-01-12 23:28:37 +08:00
|
|
|
|
key_infos.append(
|
|
|
|
|
|
RoutingKeyInfo(
|
|
|
|
|
|
id=key.id or "",
|
|
|
|
|
|
name=key.name or "",
|
|
|
|
|
|
masked_key=masked_key,
|
|
|
|
|
|
internal_priority=key.internal_priority or 0,
|
2026-01-16 17:53:27 +08:00
|
|
|
|
global_priority_by_format=key.global_priority_by_format,
|
2026-01-12 23:28:37 +08:00
|
|
|
|
rpm_limit=key.rpm_limit,
|
|
|
|
|
|
is_adaptive=is_adaptive,
|
|
|
|
|
|
effective_rpm=effective_rpm,
|
|
|
|
|
|
cache_ttl_minutes=key.cache_ttl_minutes or 0,
|
|
|
|
|
|
health_score=health_score,
|
|
|
|
|
|
is_active=bool(key.is_active),
|
|
|
|
|
|
api_formats=key.api_formats or [],
|
2026-01-13 16:04:15 +08:00
|
|
|
|
allowed_models=allowed_models_list,
|
2026-01-12 23:28:37 +08:00
|
|
|
|
circuit_breaker_open=circuit_breaker_open,
|
|
|
|
|
|
circuit_breaker_formats=circuit_breaker_formats,
|
2026-01-13 18:42:15 +08:00
|
|
|
|
next_probe_at=next_probe_at,
|
2026-01-12 23:28:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-22 10:32:06 +08:00
|
|
|
|
# 计算有效 Keys 数量:is_active 即可(模型权限已在前面过滤)
|
|
|
|
|
|
active_keys = sum(1 for k in key_infos if k.is_active)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
endpoint_infos.append(
|
|
|
|
|
|
RoutingEndpointInfo(
|
|
|
|
|
|
id=ep.id or "",
|
|
|
|
|
|
api_format=ep.api_format or "",
|
|
|
|
|
|
base_url=ep.base_url or "",
|
|
|
|
|
|
custom_path=ep.custom_path,
|
|
|
|
|
|
is_active=bool(ep.is_active),
|
|
|
|
|
|
keys=key_infos,
|
|
|
|
|
|
total_keys=len(key_infos),
|
|
|
|
|
|
active_keys=active_keys,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
# 按 endpoint signature 的推荐顺序排序 Endpoints(与前端展示保持一致)
|
|
|
|
|
|
preferred_order = [
|
|
|
|
|
|
"openai:chat",
|
|
|
|
|
|
"openai:cli",
|
2026-03-01 23:55:26 +08:00
|
|
|
|
"openai:compact",
|
2026-02-01 17:28:00 +08:00
|
|
|
|
"openai:video",
|
|
|
|
|
|
"claude:chat",
|
|
|
|
|
|
"claude:cli",
|
|
|
|
|
|
"gemini:chat",
|
|
|
|
|
|
"gemini:cli",
|
|
|
|
|
|
"gemini:video",
|
|
|
|
|
|
]
|
|
|
|
|
|
order_map = {key: i for i, key in enumerate(preferred_order)}
|
|
|
|
|
|
endpoint_infos.sort(
|
|
|
|
|
|
key=lambda e: order_map.get(str(e.api_format or "").strip().lower(), 999)
|
|
|
|
|
|
)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
active_endpoints = sum(1 for e in endpoint_infos if e.is_active)
|
|
|
|
|
|
provider_infos.append(
|
|
|
|
|
|
RoutingProviderInfo(
|
|
|
|
|
|
id=provider.id,
|
|
|
|
|
|
name=provider.name,
|
|
|
|
|
|
model_id=model.id,
|
|
|
|
|
|
provider_priority=provider.provider_priority,
|
|
|
|
|
|
billing_type=provider.billing_type,
|
|
|
|
|
|
monthly_quota_usd=provider.monthly_quota_usd,
|
|
|
|
|
|
monthly_used_usd=provider.monthly_used_usd,
|
|
|
|
|
|
is_active=bool(provider.is_active),
|
|
|
|
|
|
provider_model_name=model.provider_model_name,
|
|
|
|
|
|
model_mappings=model_mappings,
|
|
|
|
|
|
model_is_active=bool(model.is_active),
|
|
|
|
|
|
endpoints=endpoint_infos,
|
|
|
|
|
|
total_endpoints=len(endpoint_infos),
|
|
|
|
|
|
active_endpoints=active_endpoints,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 按 provider_priority 排序
|
|
|
|
|
|
provider_infos.sort(key=lambda p: p.provider_priority)
|
|
|
|
|
|
|
|
|
|
|
|
active_providers = sum(1 for p in provider_infos if p.is_active and p.model_is_active)
|
|
|
|
|
|
|
|
|
|
|
|
# 从数据库获取当前调度配置
|
2026-01-15 00:35:28 +08:00
|
|
|
|
scheduling_mode = (
|
|
|
|
|
|
SystemConfigService.get_config(
|
|
|
|
|
|
db,
|
|
|
|
|
|
"scheduling_mode",
|
|
|
|
|
|
CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY,
|
|
|
|
|
|
)
|
|
|
|
|
|
or CacheAwareScheduler.SCHEDULING_MODE_CACHE_AFFINITY
|
|
|
|
|
|
)
|
|
|
|
|
|
priority_mode = (
|
|
|
|
|
|
SystemConfigService.get_config(
|
|
|
|
|
|
db,
|
|
|
|
|
|
"provider_priority_mode",
|
|
|
|
|
|
CacheAwareScheduler.PRIORITY_MODE_PROVIDER,
|
|
|
|
|
|
)
|
|
|
|
|
|
or CacheAwareScheduler.PRIORITY_MODE_PROVIDER
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取所有活跃 Provider 的 Key 白名单数据(供前端实时匹配)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
all_keys_whitelist: list[GlobalKeyWhitelistItem] = []
|
2026-01-15 00:35:28 +08:00
|
|
|
|
crypto = CryptoService()
|
|
|
|
|
|
|
|
|
|
|
|
# 获取所有活跃的 Key(带白名单),使用 selectinload 避免 N+1 查询
|
|
|
|
|
|
all_keys = (
|
|
|
|
|
|
db.query(ProviderAPIKey)
|
|
|
|
|
|
.join(Provider, ProviderAPIKey.provider_id == Provider.id)
|
|
|
|
|
|
.options(selectinload(ProviderAPIKey.provider))
|
|
|
|
|
|
.filter(ProviderAPIKey.is_active == True)
|
|
|
|
|
|
.filter(Provider.is_active == True)
|
|
|
|
|
|
.filter(ProviderAPIKey.allowed_models.isnot(None)) # 只获取有白名单的 Key
|
|
|
|
|
|
.all()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 转换为白名单数据
|
|
|
|
|
|
for key in all_keys:
|
|
|
|
|
|
if not key.allowed_models:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 解析白名单
|
|
|
|
|
|
allowed_models_list = parse_allowed_models_to_list(key.allowed_models)
|
|
|
|
|
|
if not allowed_models_list:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 生成脱敏 Key
|
|
|
|
|
|
masked = ""
|
|
|
|
|
|
if key.api_key:
|
|
|
|
|
|
try:
|
|
|
|
|
|
decrypted = crypto.decrypt(key.api_key, silent=True)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
decrypted = key.api_key
|
|
|
|
|
|
if len(decrypted) > 8:
|
|
|
|
|
|
masked = f"{decrypted[:4]}***{decrypted[-4:]}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
masked = f"{decrypted[:2]}***"
|
|
|
|
|
|
|
|
|
|
|
|
all_keys_whitelist.append(
|
|
|
|
|
|
GlobalKeyWhitelistItem(
|
|
|
|
|
|
key_id=key.id or "",
|
|
|
|
|
|
key_name=key.name or "",
|
|
|
|
|
|
masked_key=masked,
|
|
|
|
|
|
provider_id=key.provider_id or "",
|
|
|
|
|
|
provider_name=key.provider.name if key.provider else "",
|
|
|
|
|
|
allowed_models=allowed_models_list,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-01-12 23:28:37 +08:00
|
|
|
|
|
|
|
|
|
|
return ModelRoutingPreviewResponse(
|
|
|
|
|
|
global_model_id=global_model.id,
|
|
|
|
|
|
global_model_name=global_model.name,
|
|
|
|
|
|
display_name=global_model.display_name,
|
|
|
|
|
|
is_active=bool(global_model.is_active),
|
2026-01-15 02:36:00 +08:00
|
|
|
|
global_model_mappings=global_model_mappings,
|
2026-01-12 23:28:37 +08:00
|
|
|
|
providers=provider_infos,
|
|
|
|
|
|
total_providers=len(provider_infos),
|
|
|
|
|
|
active_providers=active_providers,
|
|
|
|
|
|
scheduling_mode=scheduling_mode,
|
|
|
|
|
|
priority_mode=priority_mode,
|
2026-01-15 00:35:28 +08:00
|
|
|
|
all_keys_whitelist=all_keys_whitelist,
|
2026-01-12 23:28:37 +08:00
|
|
|
|
)
|