refactor: 将模型别名(alias)统一重命名为模型映射(mapping)

- 重命名 API: alias-mapping-preview -> mapping-preview
- 重命名类型: AliasMatchedModel -> MappingMatchedModel 等
- 重命名字段: model_aliases -> model_mappings, alias_matched_model -> mapping_matched_model
- 重命名前端组件: ModelAliasesTab -> ModelMappingsTab
- 重命名验证函数: validate_model_aliases -> validate_model_mappings
- 同步更新相关测试用例
- 补充 ModelService 中新增/批量创建模型时的缓存失效逻辑
This commit is contained in:
fawney19
2026-01-14 19:35:41 +08:00
parent 24ece00e93
commit bec9c3a989
18 changed files with 328 additions and 279 deletions

View File

@@ -77,7 +77,7 @@ class ProviderCandidate:
is_cached: bool = False
is_skipped: bool = False # 是否被跳过
skip_reason: Optional[str] = None # 跳过原因
alias_matched_model: Optional[str] = None # 通过别名匹配到的模型名(用于实际请求)
mapping_matched_model: Optional[str] = None # 通过映射匹配到的模型名(用于实际请求)
@dataclass
@@ -580,7 +580,7 @@ class CacheAwareScheduler:
target_format = normalize_api_format(api_format)
# 0. 解析 model_name 到 GlobalModel支持直接匹配和映射名匹配使用 ModelCacheService
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, model_name)
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(db, model_name)
if not global_model:
logger.warning(f"GlobalModel not found: {model_name}")
@@ -591,8 +591,8 @@ 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", [])
# 提取模型映射(用于 Provider Key 的 allowed_models 匹配)
model_mappings: List[str] = (global_model.config or {}).get("model_mappings", [])
# 获取合并后的访问限制ApiKey + User
restrictions = self._get_effective_restrictions(user_api_key)
@@ -660,7 +660,7 @@ class CacheAwareScheduler:
target_format=target_format,
model_name=requested_model_name,
resolved_model_name=resolved_model_name,
model_aliases=model_aliases,
model_mappings=model_mappings,
affinity_key=affinity_key,
max_candidates=max_candidates,
is_stream=is_stream,
@@ -774,7 +774,7 @@ class CacheAwareScheduler:
- provider_model_names: Provider 侧可用的模型名称集合(主名称 + 映射名称,按 api_format 过滤)
"""
# 使用 ModelCacheService 解析模型名称(支持映射名)
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, model_name)
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(db, model_name)
if not global_model:
# 完全未找到匹配
@@ -883,7 +883,7 @@ class CacheAwareScheduler:
model_name: str,
capability_requirements: Optional[Dict[str, bool]] = None,
resolved_model_name: Optional[str] = None,
model_aliases: Optional[List[str]] = None,
model_mappings: Optional[List[str]] = None,
candidate_models: Optional[set[str]] = None,
) -> Tuple[bool, Optional[str], Optional[str]]:
"""
@@ -897,14 +897,14 @@ class CacheAwareScheduler:
model_name: 模型名称
capability_requirements: 能力需求(可选)
resolved_model_name: 解析后的 GlobalModel.name可选
model_aliases: GlobalModel 的别名列表(用于通配符匹配)
candidate_models: Provider 侧可用的模型名称集合(用于限制别名匹配范围)
model_mappings: GlobalModel 的映射列表(用于通配符匹配)
candidate_models: Provider 侧可用的模型名称集合(用于限制映射匹配范围)
Returns:
(is_available, skip_reason, alias_matched_model)
(is_available, skip_reason, mapping_matched_model)
- is_available: Key 是否可用
- skip_reason: 不可用时的原因
- alias_matched_model: 通过别名匹配到的模型名(用于实际请求)
- mapping_matched_model: 通过映射匹配到的模型名(用于实际请求)
"""
# 检查熔断器状态(使用详细状态方法获取更丰富的跳过原因,按 API 格式)
is_available, circuit_reason = health_monitor.get_circuit_breaker_status(
@@ -915,33 +915,33 @@ class CacheAwareScheduler:
# 模型权限检查:使用 allowed_models 白名单
# None = 允许所有模型,[] = 拒绝所有模型,["a","b"] = 只允许指定模型
# 支持通配符别名匹配(通过 model_aliases
# 支持通配符映射匹配(通过 model_mappings
from src.core.model_permissions import (
check_model_allowed_with_aliases,
check_model_allowed_with_mappings,
get_allowed_models_preview,
)
try:
is_allowed, alias_matched_model = check_model_allowed_with_aliases(
is_allowed, mapping_matched_model = check_model_allowed_with_mappings(
model_name=model_name,
allowed_models=key.allowed_models,
resolved_model_name=resolved_model_name,
model_aliases=model_aliases,
model_mappings=model_mappings,
candidate_models=candidate_models,
)
except TimeoutError:
# 正则匹配超时(可能是 ReDoS 攻击或复杂模式)
logger.warning(f"别名匹配超时: key_id={key.id}, model={model_name}")
return False, "别名匹配超时,请简化配置", None
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
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)
logger.error(f"映射匹配异常: key_id={key.id}, model={model_name}, error={e}", exc_info=True)
# 异常时保守处理:不允许使用该 Key
return False, "别名匹配失败", None
return False, "映射匹配失败", None
if not is_allowed:
return False, f"模型权限不匹配(允许: {get_allowed_models_preview(key.allowed_models)})", None
@@ -957,7 +957,7 @@ class CacheAwareScheduler:
if not is_match:
return False, skip_reason, None
return True, None, alias_matched_model
return True, None, mapping_matched_model
async def _build_candidates(
self,
@@ -967,7 +967,7 @@ class CacheAwareScheduler:
model_name: str,
affinity_key: Optional[str],
resolved_model_name: Optional[str] = None,
model_aliases: Optional[List[str]] = None,
model_mappings: Optional[List[str]] = None,
max_candidates: Optional[int] = None,
is_stream: bool = False,
capability_requirements: Optional[Dict[str, bool]] = None,
@@ -984,7 +984,7 @@ class CacheAwareScheduler:
model_name: 模型名称(用户请求的名称,可能是映射名)
affinity_key: 亲和性标识符通常为API Key ID
resolved_model_name: 解析后的 GlobalModel.name用于 Key.allowed_models 校验)
model_aliases: GlobalModel 的别名列表(用于 Key.allowed_models 通配符匹配)
model_mappings: GlobalModel 的映射列表(用于 Key.allowed_models 通配符匹配)
max_candidates: 最大候选数
is_stream: 是否是流式请求,如果为 True 则过滤不支持流式的 Provider
capability_requirements: 能力需求(可选)
@@ -1047,13 +1047,13 @@ class CacheAwareScheduler:
for key in keys:
# Key 级别的能力检查
is_available, skip_reason, alias_matched_model = self._check_key_availability(
is_available, skip_reason, mapping_matched_model = self._check_key_availability(
key,
target_format_str,
model_name,
capability_requirements,
resolved_model_name=resolved_model_name,
model_aliases=model_aliases,
model_mappings=model_mappings,
candidate_models=provider_model_names,
)
@@ -1063,7 +1063,7 @@ class CacheAwareScheduler:
key=key,
is_skipped=not is_available,
skip_reason=skip_reason,
alias_matched_model=alias_matched_model,
mapping_matched_model=mapping_matched_model,
)
candidates.append(candidate)

View File

@@ -15,7 +15,7 @@ Model 映射缓存服务 - 减少模型查询
使用示例
--------
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(db, "gpt-4")
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(db, "gpt-4")
"""
import time
@@ -250,7 +250,7 @@ class ModelCacheService:
logger.debug(f"GlobalModel 缓存已清除: {global_model_id}")
@staticmethod
async def resolve_global_model_by_name_or_alias(
async def resolve_global_model_by_name_or_mapping(
db: Session, model_name: str
) -> Optional[GlobalModel]:
"""

View File

@@ -102,7 +102,7 @@ class ModelMapperMiddleware:
mapping = None
# 步骤 1: 解析 GlobalModel支持映射名
global_model = await ModelCacheService.resolve_global_model_by_name_or_alias(
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(
self.db, source_model
)

View File

@@ -77,6 +77,24 @@ class ModelService:
logger.info(f"创建模型成功: provider={provider.name}, model={model.provider_model_name}, global_model_id={model.global_model_id}")
# 清除 Redis 缓存(异步执行,不阻塞返回)
# 重要:新增模型可能需要清除 resolver 的 NOT_FOUND 负缓存global_model:resolve:*
# 否则请求链路在 TTL 内可能无法立刻解析到新模型。
asyncio.create_task(
ModelCacheService.invalidate_model_cache(
model_id=model.id,
provider_id=model.provider_id,
global_model_id=model.global_model_id,
provider_model_name=model.provider_model_name,
provider_model_mappings=model.provider_model_mappings,
)
)
# 清除内存缓存ModelMapperMiddleware 实例)
if model.provider_id and model.global_model_id:
cache_service = get_cache_invalidation_service()
cache_service.on_model_changed(model.provider_id, model.global_model_id)
# 清除 /v1/models 列表缓存
asyncio.create_task(invalidate_models_list_cache())
@@ -154,6 +172,7 @@ class ModelService:
raise NotFoundException(f"模型 {model_id} 不存在")
# 保存旧的映射,用于清除缓存
old_global_model_id = model.global_model_id
old_provider_model_name = model.provider_model_name
old_provider_model_mappings = model.provider_model_mappings
@@ -179,14 +198,17 @@ class ModelService:
ModelCacheService.invalidate_model_cache(
model_id=model.id,
provider_id=model.provider_id,
global_model_id=model.global_model_id,
global_model_id=old_global_model_id,
provider_model_name=old_provider_model_name,
provider_model_mappings=old_provider_model_mappings,
)
)
# 再清除新的映射缓存(如果有变化)
if (model.provider_model_name != old_provider_model_name or
model.provider_model_mappings != old_provider_model_mappings):
# 再清除新的映射缓存(如果有变化,包括 global_model_id 变更
if (
model.provider_model_name != old_provider_model_name
or model.provider_model_mappings != old_provider_model_mappings
or model.global_model_id != old_global_model_id
):
asyncio.create_task(
ModelCacheService.invalidate_model_cache(
model_id=model.id,
@@ -354,6 +376,7 @@ class ModelService:
provider_id=provider_id,
global_model_id=model_data.global_model_id,
provider_model_name=model_data.provider_model_name,
provider_model_mappings=model_data.provider_model_mappings,
price_per_request=model_data.price_per_request,
tiered_pricing=model_data.tiered_pricing,
supports_vision=model_data.supports_vision,
@@ -373,6 +396,23 @@ class ModelService:
db.refresh(model)
logger.info(f"批量创建 {len(created_models)} 个模型成功")
# 清除 Redis 缓存(异步执行,不阻塞返回)
# 逐个清除 resolver 的映射缓存,避免 NOT_FOUND 负缓存阻塞新模型生效。
for model in created_models:
asyncio.create_task(
ModelCacheService.invalidate_model_cache(
model_id=model.id,
provider_id=model.provider_id,
global_model_id=model.global_model_id,
provider_model_name=model.provider_model_name,
provider_model_mappings=model.provider_model_mappings,
)
)
# 清除内存缓存ModelMapperMiddleware 实例)
cache_service = get_cache_invalidation_service()
cache_service.on_model_changed(provider_id, created_models[0].global_model_id)
# 清除 /v1/models 列表缓存
asyncio.create_task(invalidate_models_list_cache())
except IntegrityError as e: