mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
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:
64
src/services/cache/aware_scheduler.py
vendored
64
src/services/cache/aware_scheduler.py
vendored
@@ -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)
|
||||
|
||||
|
||||
61
src/services/cache/invalidation.py
vendored
61
src/services/cache/invalidation.py
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user