refactor(model-permissions): 简化 allowed_models 为纯列表格式

移除按 API 格式区分的字典模式(Dict[str, List[str]]),统一使用简单列表格式。

- 删除 normalize_allowed_models 的 api_format 参数
- 删除 check_model_allowed 的 api_format 参数
- 简化 merge_allowed_models 为直接列表交集
- 移除前端的字典模式兼容代码和警告 UI
- 删除 is_format_mode、convert_to_format_mode 等辅助函数
This commit is contained in:
fawney19
2026-01-14 17:57:21 +08:00
parent 133f3108f0
commit b272109055
9 changed files with 62 additions and 345 deletions

View File

@@ -82,31 +82,19 @@ export interface ProviderEndpoint {
}
/**
* 模型权限配置类型(支持简单列表和按格式字典两种模式)
* 模型权限配置类型
*
* 使用示例:
* 1. 不限制(允许所有模型): null
* 2. 简单列表模式(所有 API 格式共享同一个白名单: ["gpt-4", "claude-3-opus"]
* 3. 按格式字典模式(不同 API 格式使用不同的白名单):
* { "OPENAI": ["gpt-4"], "CLAUDE": ["claude-3-opus"] }
* 2. 白名单模式: ["gpt-4", "claude-3-opus"]
*/
export type AllowedModels = string[] | Record<string, string[]> | null
export type AllowedModels = string[] | null
// AllowedModels 类型守卫函数
export function isAllowedModelsList(value: AllowedModels): value is string[] {
return Array.isArray(value)
}
export function isAllowedModelsDict(value: AllowedModels): value is Record<string, string[]> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return false
}
// 验证所有值都是字符串数组
return Object.values(value).every(
(v) => Array.isArray(v) && v.every((item) => typeof item === 'string')
)
}
export interface EndpointAPIKey {
id: string
provider_id: string
@@ -119,7 +107,7 @@ export interface EndpointAPIKey {
internal_priority: number // Key 内部优先级
global_priority?: number | null // 全局 Key 优先级
rpm_limit?: number | null // RPM 速率限制 (1-10000)null 表示自适应模式
allowed_models?: AllowedModels // 允许使用的模型列表null=不限制,列表=简单白名单,字典=按格式区分
allowed_models?: AllowedModels // 允许使用的模型列表null=不限制)
capabilities?: Record<string, boolean> | null // 能力标签配置(如 cache_1h, context_1m
// 缓存与熔断配置
cache_ttl_minutes: number // 缓存 TTL分钟0=禁用

View File

@@ -9,17 +9,6 @@
>
<template #default>
<div class="space-y-4">
<!-- 字典模式警告 -->
<div
v-if="isDictMode"
class="rounded-lg border border-amber-500/50 bg-amber-50 dark:bg-amber-950/30 p-3"
>
<p class="text-sm text-amber-700 dark:text-amber-400">
<strong>注意</strong>此密钥使用按 API 格式区分的模型权限配置
编辑后将转换为统一列表模式原有的格式区分信息将丢失
</p>
</div>
<!-- 常驻选择面板 -->
<div class="border rounded-lg overflow-hidden">
<!-- 搜索 + 操作栏 -->
@@ -374,9 +363,6 @@ const initialLockedModels = ref<string[]>([])
// 所有添加过的自定义模型(包括已取消勾选的,保存前不消失)
const allCustomModels = ref<string[]>([])
// 是否为字典模式(按 API 格式区分)
const isDictMode = ref(false)
// 是否为自动获取模式
const isAutoFetchMode = computed(() => props.apiKey?.auto_fetch_models ?? false)
@@ -646,20 +632,9 @@ async function fetchUpstreamModels() {
// 解析 allowed_models
function parseAllowedModels(allowed: AllowedModels): string[] {
if (allowed === null || allowed === undefined) {
isDictMode.value = false
return []
}
if (Array.isArray(allowed)) {
isDictMode.value = false
return [...allowed]
}
// 字典模式:合并所有格式的模型,并设置警告标志
isDictMode.value = true
const all = new Set<string>()
for (const models of Object.values(allowed)) {
models.forEach(m => all.add(m))
}
return Array.from(all)
return [...allowed]
}
// 监听对话框打开

View File

@@ -223,12 +223,10 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
update_data["learned_rpm_limit"] = None
logger.info("Key %s 切换为自适应 RPM 模式", self.key_id)
# 统一处理 allowed_models空列表/空字典 -> None表示不限制
# 统一处理 allowed_models空列表 -> None表示不限制
if "allowed_models" in update_data:
am = update_data["allowed_models"]
if am is not None and (
(isinstance(am, list) and len(am) == 0) or (isinstance(am, dict) and len(am) == 0)
):
if isinstance(am, list) and len(am) == 0:
update_data["allowed_models"] = None
# 统一处理 locked_models空列表 -> None

View File

@@ -310,19 +310,12 @@ class AdminGetModelRoutingPreviewAdapter(AdminApiAdapter):
next_probe_at = fmt_next_probe
# 解析 allowed_models
# 语义说明:
# - None: 不限制(允许所有模型)
# - {}: 空字典 = 不限制normalize_allowed_models 返回 None
# - []: 空列表 = 拒绝所有模型
# - {"CLAUDE": []}: 指定格式空列表 = 该格式拒绝所有
raw_allowed_models = key.allowed_models
if raw_allowed_models is None:
allowed_models_list = None
elif isinstance(raw_allowed_models, dict) and not raw_allowed_models:
# 空 dict {} 在语义上等价于不限制
allowed_models_list = None
else:
allowed_models_list = parse_allowed_models_to_list(raw_allowed_models)
allowed_models_list = (
parse_allowed_models_to_list(raw_allowed_models)
if raw_allowed_models
else None
)
key_infos.append(
RoutingKeyInfo(

View File

@@ -352,7 +352,7 @@ def _get_available_model_ids_for_format(db: Session, api_formats: list[str]) ->
if model_provider_id not in provider_ids_with_format:
continue
# 检查该 provider 下是否有 Key 允许这个模型(支持 list/dict 两种 allowed_models
# 检查该 provider 下是否有 Key 允许这个模型
from src.core.model_permissions import check_model_allowed
rules = provider_key_rules.get(model_provider_id, [])
@@ -362,19 +362,14 @@ def _get_available_model_ids_for_format(db: Session, api_formats: list[str]) ->
available_model_ids.add(model_id)
break
# 对于支持多个格式的 Key任意一个可用格式允许即可
for fmt in usable_formats:
if check_model_allowed(
model_name=model_id,
allowed_models=allowed_models, # type: ignore[arg-type]
api_format=fmt,
resolved_model_name=(model.provider_model_name if global_model else None),
):
available_model_ids.add(model_id)
break
else:
continue
break
# 检查是否允许该模型
if check_model_allowed(
model_name=model_id,
allowed_models=allowed_models, # type: ignore[arg-type]
resolved_model_name=(model.provider_model_name if global_model else None),
):
available_model_ids.add(model_id)
break
return available_model_ids

View File

@@ -1,10 +1,7 @@
"""
模型权限工具
支持两种 allowed_models 格式
1. 简单模式(列表): ["claude-sonnet-4", "gpt-4o"]
2. 按格式模式(字典): {"OPENAI": ["gpt-4o"], "CLAUDE": ["claude-sonnet-4"]}
allowed_models 格式: ["claude-sonnet-4", "gpt-4o"]
使用 None/null 表示不限制(允许所有模型)
支持模型别名匹配:
@@ -16,7 +13,7 @@
import re
from functools import lru_cache
from typing import Dict, List, Optional, Tuple, Union
from typing import List, Optional, Tuple
import regex
@@ -29,19 +26,15 @@ MAX_MODEL_NAME_LENGTH = 200 # 与 MAX_ALIAS_LENGTH 保持一致
REGEX_MATCH_TIMEOUT_MS = 100 # 正则匹配超时(毫秒)
# 类型别名
AllowedModels = Optional[Union[List[str], Dict[str, List[str]]]]
AllowedModels = Optional[List[str]]
def normalize_allowed_models(
allowed_models: AllowedModels,
api_format: Optional[str] = None,
) -> Optional[set[str]]:
def normalize_allowed_models(allowed_models: AllowedModels) -> Optional[set[str]]:
"""
将 allowed_models 规范化为模型名称集合
Args:
allowed_models: 允许的模型配置(列表或字典
api_format: 当前请求的 API 格式(用于字典模式)
allowed_models: 允许的模型配置(列表)
Returns:
- None: 不限制(允许所有模型)
@@ -50,41 +43,12 @@ def normalize_allowed_models(
if allowed_models is None:
return None
# 简单模式:直接是列表
if isinstance(allowed_models, list):
return set(allowed_models)
# 按格式模式:字典
if isinstance(allowed_models, dict):
if api_format is None:
# 没有指定格式,合并所有格式的模型
all_models: set[str] = set()
for models in allowed_models.values():
if isinstance(models, list):
all_models.update(models)
return all_models if all_models else None
# 查找指定格式的模型列表
api_format_upper = api_format.upper()
models = allowed_models.get(api_format_upper)
if models is None:
# 该格式未配置,检查是否有通配符 "*"
models = allowed_models.get("*")
if models is None:
# 字典模式下未配置的格式 = 不限制该格式
return None
return set(models) if isinstance(models, list) else None
# 未知类型,视为不限制
return None
return set(allowed_models)
def check_model_allowed(
model_name: str,
allowed_models: AllowedModels,
api_format: Optional[str] = None,
resolved_model_name: Optional[str] = None,
) -> bool:
"""
@@ -93,14 +57,13 @@ def check_model_allowed(
Args:
model_name: 请求的模型名称
allowed_models: 允许的模型配置
api_format: 当前请求的 API 格式
resolved_model_name: 解析后的 GlobalModel.name可选
Returns:
True: 允许使用该模型
False: 不允许使用该模型
"""
allowed_set = normalize_allowed_models(allowed_models, api_format)
allowed_set = normalize_allowed_models(allowed_models)
if allowed_set is None:
# 不限制
@@ -130,8 +93,6 @@ def merge_allowed_models(
规则:
- 如果任一为 None返回另一个
- 如果都有值,取交集
- 如果都是列表,取列表交集
- 如果有字典,按 API 格式分别取交集(保持字典语义,不丢失格式区分信息)
Args:
allowed_models_1: 第一个配置
@@ -145,57 +106,8 @@ def merge_allowed_models(
if allowed_models_2 is None:
return allowed_models_1
# 两个都是简单列表:直接取交集(返回确定性顺序)
if isinstance(allowed_models_1, list) and isinstance(allowed_models_2, list):
intersection = set(allowed_models_1) & set(allowed_models_2)
return sorted(intersection) if intersection else []
# 任一为字典模式:按 API 格式分别取交集,避免把 dict 合并成 list 导致权限过宽
from src.core.enums import APIFormat
def merge_sets(a: Optional[set[str]], b: Optional[set[str]]) -> Optional[set[str]]:
# None 表示不限制:交集规则下等价于“只受另一方限制”
if a is None:
return b
if b is None:
return a
return a & b
known_formats = [fmt.value for fmt in APIFormat]
per_format: Dict[str, Optional[set[str]]] = {}
for fmt in known_formats:
s1 = normalize_allowed_models(allowed_models_1, api_format=fmt)
s2 = normalize_allowed_models(allowed_models_2, api_format=fmt)
per_format[fmt] = merge_sets(s1, s2)
# 计算默认(未知格式)的交集,用 "*" 作为默认值以覆盖未枚举的格式
default_s1 = normalize_allowed_models(allowed_models_1, api_format="__DEFAULT__")
default_s2 = normalize_allowed_models(allowed_models_2, api_format="__DEFAULT__")
default_set = merge_sets(default_s1, default_s2)
# 如果 default_set 非 None 且不存在“某些格式不限制”的情况,可用 "*" 作为默认规则并按需覆盖
can_use_wildcard = default_set is not None and all(v is not None for v in per_format.values())
merged_dict: Dict[str, List[str]] = {}
if can_use_wildcard and default_set is not None:
merged_dict["*"] = sorted(default_set)
for fmt, s in per_format.items():
# can_use_wildcard 保证 s 非 None
if s is not None and s != default_set:
merged_dict[fmt] = sorted(s)
else:
for fmt, s in per_format.items():
if s is None:
continue
merged_dict[fmt] = sorted(s)
if not merged_dict:
# 全部不限制
return None
return merged_dict
intersection = set(allowed_models_1) & set(allowed_models_2)
return sorted(intersection) if intersection else []
def get_allowed_models_preview(
@@ -215,19 +127,10 @@ def get_allowed_models_preview(
if allowed_models is None:
return "(不限制)"
all_models: set[str] = set()
if isinstance(allowed_models, list):
all_models = set(allowed_models)
elif isinstance(allowed_models, dict):
for models in allowed_models.values():
if isinstance(models, list):
all_models.update(models)
if not all_models:
if not allowed_models:
return "(无)"
sorted_models = sorted(all_models)
sorted_models = sorted(allowed_models)
preview = ", ".join(sorted_models[:max_items])
if len(sorted_models) > max_items:
preview += f", ...共{len(sorted_models)}"
@@ -235,103 +138,20 @@ def get_allowed_models_preview(
return preview
def is_format_mode(allowed_models: AllowedModels) -> bool:
def parse_allowed_models_to_list(allowed_models: AllowedModels) -> List[str]:
"""
判断 allowed_models 是否为按格式模式
解析 allowed_models 为列表
Args:
allowed_models: 允许的模型配置
Returns:
True: 按格式模式(字典)
False: 简单模式(列表或 None
"""
return isinstance(allowed_models, dict)
def convert_to_format_mode(
allowed_models: AllowedModels,
api_formats: Optional[List[str]] = None,
) -> Dict[str, List[str]]:
"""
将 allowed_models 转换为按格式模式
Args:
allowed_models: 原始配置
api_formats: 要应用的 API 格式列表
Returns:
按格式模式的配置
"""
if allowed_models is None:
return {}
if isinstance(allowed_models, dict):
return allowed_models
# 简单列表模式 -> 按格式模式
if isinstance(allowed_models, list):
if not api_formats:
return {"*": allowed_models}
return {fmt.upper(): list(allowed_models) for fmt in api_formats}
return {}
def convert_to_simple_mode(allowed_models: AllowedModels) -> Optional[List[str]]:
"""
将 allowed_models 转换为简单列表模式
Args:
allowed_models: 原始配置
Returns:
简单列表或 None
"""
if allowed_models is None:
return None
if isinstance(allowed_models, list):
return allowed_models
if isinstance(allowed_models, dict):
all_models: set[str] = set()
for models in allowed_models.values():
if isinstance(models, list):
all_models.update(models)
return sorted(all_models) if all_models else None
return None
def parse_allowed_models_to_list(allowed_models: AllowedModels) -> List[str]:
"""
解析 allowed_models支持 list 和 dict 格式)为统一的列表
与 convert_to_simple_mode 的区别:
- 本函数返回空列表而非 None用于 UI 展示)
- convert_to_simple_mode 返回 None 表示不限制
Args:
allowed_models: 允许的模型配置(列表或字典)
Returns:
模型名称列表(可能为空)
"""
if allowed_models is None:
return []
if isinstance(allowed_models, list):
return allowed_models
if isinstance(allowed_models, dict):
all_models: set[str] = set()
for models in allowed_models.values():
if isinstance(models, list):
all_models.update(models)
return sorted(all_models)
return []
return list(allowed_models)
def validate_alias_pattern(pattern: str) -> Tuple[bool, Optional[str]]:
@@ -530,7 +350,6 @@ def match_model_with_pattern(pattern: str, model_name: str) -> bool:
def check_model_allowed_with_aliases(
model_name: str,
allowed_models: AllowedModels,
api_format: Optional[str] = None,
resolved_model_name: Optional[str] = None,
model_aliases: Optional[List[str]] = None,
candidate_models: Optional[set[str]] = None,
@@ -552,7 +371,6 @@ def check_model_allowed_with_aliases(
Args:
model_name: 请求的模型名称
allowed_models: 允许的模型配置(来自 Provider Key
api_format: 当前请求的 API 格式
resolved_model_name: 解析后的 GlobalModel.name
model_aliases: GlobalModel 的别名列表(来自 config.model_aliases
candidate_models: 可选的候选模型集合(用于限制别名匹配只能落到这些模型名上)
@@ -563,7 +381,7 @@ def check_model_allowed_with_aliases(
- matched_model_name: 通过别名匹配到的模型名(仅别名匹配时有值,精确匹配时为 None
"""
# 先尝试精确匹配(使用原有逻辑)
if check_model_allowed(model_name, allowed_models, api_format, resolved_model_name):
if check_model_allowed(model_name, allowed_models, resolved_model_name):
return True, None
# 如果精确匹配失败且有别名配置,尝试别名匹配
@@ -571,7 +389,7 @@ def check_model_allowed_with_aliases(
return False, None
# 获取 allowed_models 的集合
allowed_set = normalize_allowed_models(allowed_models, api_format)
allowed_set = normalize_allowed_models(allowed_models)
if allowed_set is None:
# 不限制,已在 check_model_allowed 中返回 True
return True, None

View File

@@ -4,7 +4,7 @@ ProviderEndpoint 相关的 API 模型定义
import re
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -145,9 +145,9 @@ class EndpointAPIKeyCreate(BaseModel):
rpm_limit: Optional[int] = Field(
default=None, ge=1, le=10000, description="RPM 限制NULL=自适应模式)"
)
allowed_models: Optional[Union[List[str], Dict[str, List[str]]]] = Field(
allowed_models: Optional[List[str]] = Field(
default=None,
description="允许使用的模型列表null=不限制,列表=简单白名单,字典=按API格式区分",
description="允许使用的模型列表null=不限制)",
)
# 能力标签
@@ -200,67 +200,27 @@ class EndpointAPIKeyCreate(BaseModel):
@field_validator("allowed_models")
@classmethod
def validate_allowed_models(
cls, v: Optional[Union[List[str], Dict[str, List[str]]]]
) -> Optional[Union[List[str], Dict[str, List[str]]]]:
def validate_allowed_models(cls, v: Optional[List[str]]) -> Optional[List[str]]:
"""
规范化 allowed_models
- 列表模式:去空、去重、保留顺序
- 字典模式key 统一大写(支持 "*"value 去空、去重、保留顺序
规范化 allowed_models去空、去重、保留顺序
"""
if v is None:
return v
if isinstance(v, list):
cleaned: List[str] = []
seen: set[str] = set()
for item in v:
if not isinstance(item, str):
raise ValueError("allowed_models 列表必须为字符串数组")
name = item.strip()
if not name or name in seen:
continue
seen.add(name)
cleaned.append(name)
return cleaned
if not isinstance(v, list):
raise ValueError("allowed_models 必须是列表")
if isinstance(v, dict):
from src.core.enums import APIFormat
allowed_formats = {fmt.value for fmt in APIFormat}
normalized: Dict[str, List[str]] = {}
for raw_key, models in v.items():
if not isinstance(raw_key, str):
raise ValueError("allowed_models 字典的 key 必须为字符串")
key = raw_key.upper()
if key != "*" and key not in allowed_formats:
raise ValueError(
f"allowed_models 字典的 key 必须是 {sorted(allowed_formats)}'*',当前值: {raw_key}"
)
if models is None:
# null 表示该格式不限制,跳过(不加入字典)
continue
if not isinstance(models, list):
raise ValueError("allowed_models 字典的 value 必须为字符串数组")
cleaned: List[str] = []
seen: set[str] = set()
for item in models:
if not isinstance(item, str):
raise ValueError("allowed_models 字典的 value 必须为字符串数组")
name = item.strip()
if not name or name in seen:
continue
seen.add(name)
cleaned.append(name)
normalized[key] = cleaned
return normalized
raise ValueError("allowed_models 必须是列表或字典")
cleaned: List[str] = []
seen: set[str] = set()
for item in v:
if not isinstance(item, str):
raise ValueError("allowed_models 列表元素必须为字符串")
name = item.strip()
if not name or name in seen:
continue
seen.add(name)
cleaned.append(name)
return cleaned
@field_validator("api_key")
@classmethod
@@ -330,9 +290,9 @@ class EndpointAPIKeyUpdate(BaseModel):
rpm_limit: Optional[int] = Field(
default=None, ge=1, le=10000, description="RPM 限制null=自适应模式)"
)
allowed_models: Optional[Union[List[str], Dict[str, List[str]]]] = Field(
allowed_models: Optional[List[str]] = Field(
default=None,
description="允许使用的模型列表null=不限制,列表=简单白名单,字典=按API格式区分",
description="允许使用的模型列表null=不限制)",
)
capabilities: Optional[Dict[str, bool]] = Field(
default=None, description="Key 能力标签,如 {'cache_1h': true, 'context_1m': true}"
@@ -376,9 +336,7 @@ class EndpointAPIKeyUpdate(BaseModel):
@field_validator("allowed_models")
@classmethod
def validate_allowed_models(
cls, v: Optional[Union[List[str], Dict[str, List[str]]]]
) -> Optional[Union[List[str], Dict[str, List[str]]]]:
def validate_allowed_models(cls, v: Optional[List[str]]) -> Optional[List[str]]:
# 与 EndpointAPIKeyCreate 保持一致
return EndpointAPIKeyCreate.validate_allowed_models(v)
@@ -450,7 +408,7 @@ class EndpointAPIKeyResponse(BaseModel):
internal_priority: int = Field(default=50, description="Endpoint 内部优先级")
global_priority: Optional[int] = Field(default=None, description="全局 Key 优先级")
rpm_limit: Optional[int] = None
allowed_models: Optional[Union[List[str], Dict[str, List[str]]]] = None
allowed_models: Optional[List[str]] = None
capabilities: Optional[Dict[str, bool]] = Field(default=None, description="Key 能力标签")
# 缓存与熔断配置

View File

@@ -524,8 +524,7 @@ class CacheAwareScheduler:
user_api_key.allowed_providers, user.allowed_providers if user else None
)
# 合并 allowed_models
# allowed_models 支持 list/dict 两种结构,不能转成 set 否则会导致权限校验失效
# 合并 allowed_models(取交集)
from src.core.model_permissions import merge_allowed_models
result["allowed_models"] = merge_allowed_models(
@@ -612,13 +611,12 @@ class CacheAwareScheduler:
)
return [], global_model_id
# 0.2 检查模型是否被允许(支持简单列表和按格式字典两种模式)
# 0.2 检查模型是否被允许
from src.core.model_permissions import check_model_allowed, get_allowed_models_preview
if not check_model_allowed(
model_name=requested_model_name,
allowed_models=allowed_models,
api_format=target_format.value,
resolved_model_name=resolved_model_name,
):
resolved_note = (
@@ -915,7 +913,7 @@ class CacheAwareScheduler:
if not is_available:
return False, circuit_reason or "熔断器已打开", None
# 模型权限检查:使用 allowed_models 白名单(支持简单列表和按格式字典两种模式)
# 模型权限检查:使用 allowed_models 白名单
# None = 允许所有模型,[] = 拒绝所有模型,["a","b"] = 只允许指定模型
# 支持通配符别名匹配(通过 model_aliases
from src.core.model_permissions import (
@@ -927,7 +925,6 @@ class CacheAwareScheduler:
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,
candidate_models=candidate_models,

View File

@@ -6,7 +6,6 @@ class TestCheckModelAllowedWithAliases:
is_allowed, matched = check_model_allowed_with_aliases(
model_name="gpt-4o",
allowed_models=["gpt-4o"],
api_format="OPENAI",
resolved_model_name="gpt-4o",
model_aliases=[r"gpt-4o-.*"],
)
@@ -17,7 +16,6 @@ class TestCheckModelAllowedWithAliases:
is_allowed, matched = check_model_allowed_with_aliases(
model_name="target",
allowed_models=["b", "a"],
api_format="OPENAI",
resolved_model_name="target",
model_aliases=[r".*"],
)
@@ -28,7 +26,6 @@ class TestCheckModelAllowedWithAliases:
is_allowed, matched = check_model_allowed_with_aliases(
model_name="target",
allowed_models=["other-1", "allowed-1"],
api_format="OPENAI",
resolved_model_name="target",
model_aliases=[r".*-1"],
candidate_models={"allowed-1"},
@@ -40,11 +37,9 @@ class TestCheckModelAllowedWithAliases:
is_allowed, matched = check_model_allowed_with_aliases(
model_name="target",
allowed_models=["allowed-1"],
api_format="OPENAI",
resolved_model_name="target",
model_aliases=[r".*-1"],
candidate_models={"not-present"},
)
assert is_allowed is False
assert matched is None