feat: 实现 GlobalModel 别名匹配系统

主要更改:
- GlobalModel 支持 model_aliases 配置,允许使用正则表达式定义别名规则
- Provider Key 的 allowed_models 现在可以通过别名规则匹配 GlobalModel
- 新增 ModelAliasesTab 组件用于管理模型别名配置
- Provider 详情页新增别名映射预览功能,展示 Key 白名单与 GlobalModel 别名的匹配关系
- 路由预览 API 返回 Key 的 allowed_models 信息

安全特性:
- 使用 regex 库的原生超时保护(100ms)防止 ReDoS 攻击
- 别名规则数量限制(50 条/模型)和长度限制(200 字符)
- 别名映射预览 API 添加超时保护和结果截断

其他改进:
- GlobalModel 更新/删除时使用行级锁防止并发竞态
- 缓存失效逻辑优化,支持异步清理和正则缓存清空
- 路由 Tab 布局重构,使用 flexbox 替代绝对定位
This commit is contained in:
fawney19
2026-01-13 16:04:15 +08:00
parent 9fea71a70c
commit 85decd7487
21 changed files with 3845 additions and 2308 deletions

View File

@@ -32,6 +32,7 @@ from __future__ import annotations
import hashlib
import random
import re
import time
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
@@ -76,6 +77,7 @@ class ProviderCandidate:
is_cached: bool = False
is_skipped: bool = False # 是否被跳过
skip_reason: Optional[str] = None # 跳过原因
alias_matched_model: Optional[str] = None # 通过别名匹配到的模型名(用于实际请求)
@dataclass
@@ -590,6 +592,9 @@ class CacheAwareScheduler:
requested_model_name = model_name
resolved_model_name = str(global_model.name)
# 提取模型别名(用于 Provider Key 的 allowed_models 匹配)
model_aliases: List[str] = (global_model.config or {}).get("model_aliases", [])
# 获取合并后的访问限制ApiKey + User
restrictions = self._get_effective_restrictions(user_api_key)
allowed_api_formats = restrictions["allowed_api_formats"]
@@ -657,6 +662,7 @@ class CacheAwareScheduler:
target_format=target_format,
model_name=requested_model_name,
resolved_model_name=resolved_model_name,
model_aliases=model_aliases,
affinity_key=affinity_key,
max_candidates=max_candidates,
is_stream=is_stream,
@@ -852,7 +858,8 @@ class CacheAwareScheduler:
model_name: str,
capability_requirements: Optional[Dict[str, bool]] = None,
resolved_model_name: Optional[str] = None,
) -> Tuple[bool, Optional[str]]:
model_aliases: Optional[List[str]] = None,
) -> Tuple[bool, Optional[str], Optional[str]]:
"""
检查 API Key 的可用性
@@ -864,28 +871,53 @@ class CacheAwareScheduler:
model_name: 模型名称
capability_requirements: 能力需求(可选)
resolved_model_name: 解析后的 GlobalModel.name可选
model_aliases: GlobalModel 的别名列表(用于通配符匹配)
Returns:
(is_available, skip_reason)
(is_available, skip_reason, alias_matched_model)
- is_available: Key 是否可用
- skip_reason: 不可用时的原因
- alias_matched_model: 通过别名匹配到的模型名(用于实际请求)
"""
# 检查熔断器状态(使用详细状态方法获取更丰富的跳过原因,按 API 格式)
is_available, circuit_reason = health_monitor.get_circuit_breaker_status(
key, api_format=api_format
)
if not is_available:
return False, circuit_reason or "熔断器已打开"
return False, circuit_reason or "熔断器已打开", None
# 模型权限检查:使用 allowed_models 白名单(支持简单列表和按格式字典两种模式)
# None = 允许所有模型,[] = 拒绝所有模型,["a","b"] = 只允许指定模型
from src.core.model_permissions import check_model_allowed, get_allowed_models_preview
# 支持通配符别名匹配(通过 model_aliases
from src.core.model_permissions import (
check_model_allowed_with_aliases,
get_allowed_models_preview,
)
if not check_model_allowed(
model_name=model_name,
allowed_models=key.allowed_models,
api_format=api_format,
resolved_model_name=resolved_model_name,
):
return False, f"模型权限不匹配(允许: {get_allowed_models_preview(key.allowed_models)})"
try:
is_allowed, alias_matched_model = check_model_allowed_with_aliases(
model_name=model_name,
allowed_models=key.allowed_models,
api_format=api_format,
resolved_model_name=resolved_model_name,
model_aliases=model_aliases,
)
except TimeoutError:
# 正则匹配超时(可能是 ReDoS 攻击或复杂模式)
logger.warning(f"别名匹配超时: key_id={key.id}, model={model_name}")
return False, "别名匹配超时,请简化配置", None
except re.error as e:
# 正则语法错误(配置问题)
logger.warning(f"别名规则无效: key_id={key.id}, model={model_name}, error={e}")
return False, f"别名规则无效: {str(e)}", None
except Exception as e:
# 其他未知异常
logger.error(f"别名匹配异常: key_id={key.id}, model={model_name}, error={e}", exc_info=True)
# 异常时保守处理:不允许使用该 Key
return False, "别名匹配失败", None
if not is_allowed:
return False, f"模型权限不匹配(允许: {get_allowed_models_preview(key.allowed_models)})", None
# Key 级别的能力匹配检查
# 注意:模型级别的能力检查已在 _check_model_support 中完成
@@ -896,9 +928,9 @@ class CacheAwareScheduler:
key_caps: Dict[str, bool] = dict(key.capabilities or {})
is_match, skip_reason = check_capability_match(key_caps, capability_requirements)
if not is_match:
return False, skip_reason
return False, skip_reason, None
return True, None
return True, None, alias_matched_model
async def _build_candidates(
self,
@@ -908,6 +940,7 @@ class CacheAwareScheduler:
model_name: str,
affinity_key: Optional[str],
resolved_model_name: Optional[str] = None,
model_aliases: Optional[List[str]] = None,
max_candidates: Optional[int] = None,
is_stream: bool = False,
capability_requirements: Optional[Dict[str, bool]] = None,
@@ -924,6 +957,7 @@ class CacheAwareScheduler:
model_name: 模型名称(用户请求的名称,可能是映射名)
affinity_key: 亲和性标识符通常为API Key ID
resolved_model_name: 解析后的 GlobalModel.name用于 Key.allowed_models 校验)
model_aliases: GlobalModel 的别名列表(用于 Key.allowed_models 通配符匹配)
max_candidates: 最大候选数
is_stream: 是否是流式请求,如果为 True 则过滤不支持流式的 Provider
capability_requirements: 能力需求(可选)
@@ -981,12 +1015,13 @@ class CacheAwareScheduler:
for key in keys:
# Key 级别的能力检查
is_available, skip_reason = self._check_key_availability(
is_available, skip_reason, alias_matched_model = self._check_key_availability(
key,
target_format_str,
model_name,
capability_requirements,
resolved_model_name=resolved_model_name,
model_aliases=model_aliases,
)
candidate = ProviderCandidate(
@@ -995,6 +1030,7 @@ class CacheAwareScheduler:
key=key,
is_skipped=not is_available,
skip_reason=skip_reason,
alias_matched_model=alias_matched_model,
)
candidates.append(candidate)

View File

@@ -1,10 +1,7 @@
"""
缓存失效服务
统一管理各种缓存的失效逻辑,支持:
1. GlobalModel 变更时失效相关缓存
2. Model 变更时失效模型映射缓存
3. 支持同步和异步缓存后端
统一管理各种缓存的失效逻辑
"""
from typing import Optional
@@ -13,56 +10,54 @@ from src.core.logger import logger
class CacheInvalidationService:
"""
缓存失效服务
提供统一的缓存失效接口,当数据库模型变更时自动清理相关缓存
"""
"""缓存失效服务"""
def __init__(self):
"""初始化缓存失效服务"""
self._model_mappers = [] # 可能有多个 ModelMapperMiddleware 实例
self._model_mappers = []
def register_model_mapper(self, model_mapper):
"""注册 ModelMapper 实例"""
if model_mapper not in self._model_mappers:
self._model_mappers.append(model_mapper)
logger.debug(f"[CacheInvalidation] ModelMapper 已注册 (实例: {id(model_mapper)},总数: {len(self._model_mappers)})")
def on_global_model_changed(self, model_name: str):
async def on_global_model_changed(
self, model_name: str, global_model_id: Optional[str] = None
) -> None:
"""
GlobalModel 变更时的缓存失效
Args:
model_name: 变更的 GlobalModel.name
global_model_id: GlobalModel ID可选
"""
logger.info(f"[CacheInvalidation] GlobalModel 变更: {model_name}")
# 失效所有 ModelMapper 中与此模型相关的缓存
# 1. 清空正则缓存
from src.core.model_permissions import clear_regex_cache
clear_regex_cache()
# 2. 清空 ModelMapper 缓存
for mapper in self._model_mappers:
# 清空所有缓存(因为不知道哪些 provider 使用了这个模型)
mapper.clear_cache()
logger.debug(f"[CacheInvalidation] 已清空 ModelMapper 缓存")
# 3. 清空 ModelCacheService 缓存
from src.services.cache.model_cache import ModelCacheService
try:
await ModelCacheService.invalidate_global_model_cache(
global_model_id=global_model_id or "", name=model_name
)
except Exception as e:
logger.error(f"[CacheInvalidation] 失效 ModelCacheService 缓存失败: {e}")
def on_model_changed(self, provider_id: str, global_model_id: str):
"""
Model 变更时的缓存失效
Args:
provider_id: Provider ID
global_model_id: GlobalModel ID
"""
logger.info(f"[CacheInvalidation] Model 变更: provider={provider_id[:8]}..., "
f"global_model={global_model_id[:8]}...")
# 失效 ModelMapper 中特定 Provider 的缓存
"""Model 变更时的缓存失效"""
for mapper in self._model_mappers:
mapper.refresh_cache(provider_id)
def clear_all_caches(self):
"""清空所有缓存"""
logger.info("[CacheInvalidation] 清空所有缓存")
for mapper in self._model_mappers:
mapper.clear_cache()
@@ -72,16 +67,10 @@ _cache_invalidation_service: Optional[CacheInvalidationService] = None
def get_cache_invalidation_service() -> CacheInvalidationService:
"""
获取全局缓存失效服务实例
Returns:
CacheInvalidationService 实例
"""
"""获取全局缓存失效服务实例"""
global _cache_invalidation_service
if _cache_invalidation_service is None:
_cache_invalidation_service = CacheInvalidationService()
logger.debug("[CacheInvalidation] 初始化缓存失效服务")
return _cache_invalidation_service

View File

@@ -138,7 +138,7 @@ class RequestExecutor:
context.concurrent_requests = key_rpm_count # 用于记录,实际是 RPM 计数
context.start_time = time.time()
response = await request_func(provider, endpoint, key)
response = await request_func(provider, endpoint, key, candidate)
context.elapsed_ms = int((time.time() - context.start_time) * 1000)