feat: 模型列表 API 支持格式转换兼容性过滤

- 新增 get_compatible_provider_formats 函数,基于端点 format_acceptance_config 过滤兼容的 Provider
- 模型列表查询根据客户端格式和全局转换开关返回可用模型
- 缓存 key 增加 client_format 维度,避免不同格式的缓存混用
- GlobalModel 解析支持 provider_model_mappings 和 model_mappings 匹配
- 修复格式兼容性检查中 config 类型校验缺失问题
This commit is contained in:
fawney19
2026-01-23 14:33:42 +08:00
parent 08a3148ab3
commit 86b9bc3036
4 changed files with 425 additions and 54 deletions

View File

@@ -17,28 +17,28 @@ from sqlalchemy.orm import Session
from src.config.constants import CacheTTL
from src.core.cache_service import CacheService
from src.core.api_format.conversion.compatibility import is_format_compatible
from src.core.logger import logger
from src.services.model.availability import ModelAvailabilityQuery
from src.models.database import (
ApiKey,
Model,
User,
)
from src.models.database import ApiKey, Model, Provider, ProviderEndpoint, User
# 缓存 key 前缀
_CACHE_KEY_PREFIX = "models:list"
_CACHE_TTL = CacheTTL.MODEL # 300 秒
def _get_cache_key(api_formats: list[str]) -> str:
def _get_cache_key(api_formats: list[str], client_format: Optional[str] = None) -> str:
"""生成缓存 key"""
formats_str = ",".join(sorted(api_formats))
return f"{_CACHE_KEY_PREFIX}:{formats_str}"
format_key = (client_format or "any").lower()
return f"{_CACHE_KEY_PREFIX}:{format_key}:{formats_str}"
async def _get_cached_models(api_formats: list[str]) -> Optional[list["ModelInfo"]]:
async def _get_cached_models(
api_formats: list[str], client_format: Optional[str] = None
) -> Optional[list["ModelInfo"]]:
"""从缓存获取模型列表"""
cache_key = _get_cache_key(api_formats)
cache_key = _get_cache_key(api_formats, client_format)
try:
cached = await CacheService.get(cache_key)
if cached:
@@ -49,9 +49,13 @@ async def _get_cached_models(api_formats: list[str]) -> Optional[list["ModelInfo
return None
async def _set_cached_models(api_formats: list[str], models: list["ModelInfo"]) -> None:
async def _set_cached_models(
api_formats: list[str],
models: list["ModelInfo"],
client_format: Optional[str] = None,
) -> None:
"""将模型列表写入缓存"""
cache_key = _get_cache_key(api_formats)
cache_key = _get_cache_key(api_formats, client_format)
try:
data = [asdict(m) for m in models]
await CacheService.set(cache_key, data, ttl_seconds=_CACHE_TTL)
@@ -189,7 +193,117 @@ class AccessRestrictions:
return True
def get_available_provider_ids(db: Session, api_formats: list[str]) -> set[str]:
def _normalize_api_formats(
api_formats: Optional[list[str]],
provider_to_formats: Optional[dict[str, set[str]]] = None,
) -> list[str]:
"""规范化 API 格式列表(大写),必要时从 provider_to_formats 兜底"""
if api_formats:
return [str(fmt).upper() for fmt in api_formats if fmt is not None]
if not provider_to_formats:
return []
all_formats: set[str] = set()
for formats in provider_to_formats.values():
all_formats.update(str(fmt).upper() for fmt in formats)
return list(all_formats)
def _get_provider_model_names_for_formats(
model: Model, usable_formats: Optional[set[str]] = None
) -> set[str]:
"""
获取模型在指定格式下支持的 Provider 模型名称集合
用于 check_model_allowed_with_mappings 的 candidate_models 参数,
确保权限检查时只考虑当前格式支持的模型名。
"""
names: set[str] = {model.provider_model_name}
raw_mappings = model.provider_model_mappings
if not isinstance(raw_mappings, list):
return names
usable_formats_upper = {f.upper() for f in usable_formats} if usable_formats else None
for raw in raw_mappings:
if not isinstance(raw, dict):
continue
name = raw.get("name")
if not isinstance(name, str) or not name.strip():
continue
mapping_api_formats = raw.get("api_formats")
if usable_formats_upper and mapping_api_formats:
if isinstance(mapping_api_formats, list):
mapping_formats = {
str(fmt).upper()
for fmt in mapping_api_formats
if isinstance(fmt, str)
}
if not mapping_formats & usable_formats_upper:
continue
names.add(name.strip())
return names
def get_compatible_provider_formats(
db: Session,
client_format: str,
api_formats: list[str],
global_conversion_enabled: bool,
) -> dict[str, set[str]]:
"""
获取与客户端格式兼容的 Provider -> formats 映射
兼容性基于端点格式、format_acceptance_config 与全局格式转换开关。
"""
normalized_formats = _normalize_api_formats(api_formats)
if not normalized_formats:
return {}
target_formats = set(normalized_formats)
client_format_upper = client_format.upper()
endpoint_rows = (
db.query(
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.format_acceptance_config,
)
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
.filter(
Provider.is_active.is_(True),
ProviderEndpoint.is_active.is_(True),
ProviderEndpoint.api_format.in_(list(target_formats)),
)
.all()
)
provider_to_formats: dict[str, set[str]] = {}
for provider_id, endpoint_format, format_acceptance_config in endpoint_rows:
if not provider_id or not endpoint_format:
continue
is_compatible, _needs_conversion, _reason = is_format_compatible(
client_format_upper,
str(endpoint_format),
format_acceptance_config,
is_stream=False,
global_conversion_enabled=global_conversion_enabled,
)
if not is_compatible:
continue
fmt_upper = str(endpoint_format).upper()
provider_to_formats.setdefault(provider_id, set()).add(fmt_upper)
return provider_to_formats
def get_available_provider_ids(
db: Session,
api_formats: list[str],
provider_to_formats: Optional[dict[str, set[str]]] = None,
) -> set[str]:
"""
返回有可用端点的 Provider IDs
@@ -198,19 +312,27 @@ def get_available_provider_ids(db: Session, api_formats: list[str]) -> set[str]:
- 端点是活跃的
- Provider 下有活跃的 Key 且支持该 api_formatKey 直属 Provider通过 api_formats 过滤)
"""
provider_to_formats = ModelAvailabilityQuery.get_providers_with_active_endpoints(db, api_formats)
normalized_formats = _normalize_api_formats(api_formats, provider_to_formats)
if provider_to_formats is None:
provider_to_formats = ModelAvailabilityQuery.get_providers_with_active_endpoints(
db, normalized_formats
)
if not provider_to_formats:
return set()
return ModelAvailabilityQuery.get_providers_with_active_keys(
db,
set(provider_to_formats.keys()),
api_formats,
normalized_formats,
provider_to_formats,
)
def _get_available_model_ids_for_format(db: Session, api_formats: list[str]) -> set[str]:
def _get_available_model_ids_for_format(
db: Session,
api_formats: list[str],
provider_to_formats: Optional[dict[str, set[str]]] = None,
) -> set[str]:
"""
获取指定格式下真正可用的模型 ID 集合
@@ -220,14 +342,18 @@ def _get_available_model_ids_for_format(db: Session, api_formats: list[str]) ->
3. **该端点的 Provider 关联了该模型**
4. Key 的 allowed_models 允许该模型null = 允许该 Provider 关联的所有模型)
"""
provider_to_formats = ModelAvailabilityQuery.get_providers_with_active_endpoints(db, api_formats)
normalized_formats = _normalize_api_formats(api_formats, provider_to_formats)
if provider_to_formats is None:
provider_to_formats = ModelAvailabilityQuery.get_providers_with_active_endpoints(
db, normalized_formats
)
if not provider_to_formats:
return set()
provider_key_rules = ModelAvailabilityQuery.get_provider_key_rules(
db,
provider_ids=set(provider_to_formats.keys()),
api_formats=api_formats,
api_formats=normalized_formats,
provider_to_endpoint_formats=provider_to_formats,
)
@@ -260,17 +386,19 @@ def _get_available_model_ids_for_format(db: Session, api_formats: list[str]) ->
model_mappings = (global_model.config or {}).get("model_mappings")
rules = provider_key_rules.get(model_provider_id, [])
for allowed_models, _usable_formats in rules:
for allowed_models, usable_formats in rules:
# None = 不限制
if allowed_models is None:
available_model_ids.add(model_id)
break
# 检查是否允许该模型(支持 model_mappings 正则匹配)
candidate_models = _get_provider_model_names_for_formats(model, usable_formats)
is_allowed, _ = check_model_allowed_with_mappings(
model_name=model_id,
allowed_models=allowed_models,
model_mappings=model_mappings,
candidate_models=candidate_models,
)
if is_allowed:
available_model_ids.add(model_id)
@@ -335,6 +463,8 @@ async def list_available_models(
available_provider_ids: set[str],
api_formats: Optional[list[str]] = None,
restrictions: Optional[AccessRestrictions] = None,
provider_to_formats: Optional[dict[str, set[str]]] = None,
client_format: Optional[str] = None,
) -> list[ModelInfo]:
"""
获取可用模型列表(已去重,带缓存)
@@ -344,6 +474,8 @@ async def list_available_models(
available_provider_ids: 有可用端点的 Provider ID 集合
api_formats: API 格式列表,用于检查 Key 的 allowed_models
restrictions: API Key/User 的访问限制
provider_to_formats: Provider -> formats 映射(兼容转换过滤用)
client_format: 客户端格式(用于缓存隔离)
Returns:
去重后的 ModelInfo 列表,按创建时间倒序
@@ -359,16 +491,20 @@ async def list_available_models(
restrictions.allowed_providers is None and restrictions.allowed_models is None
)
normalized_formats = _normalize_api_formats(api_formats, provider_to_formats)
# 尝试从缓存获取
if api_formats and use_cache:
cached = await _get_cached_models(api_formats)
if normalized_formats and use_cache:
cached = await _get_cached_models(normalized_formats, client_format)
if cached is not None:
return cached
# 如果提供了 api_formats获取真正可用的模型 ID
available_model_ids: Optional[set[str]] = None
if api_formats:
available_model_ids = _get_available_model_ids_for_format(db, api_formats)
if normalized_formats:
available_model_ids = _get_available_model_ids_for_format(
db, normalized_formats, provider_to_formats
)
if not available_model_ids:
return []
@@ -399,12 +535,11 @@ async def list_available_models(
if info.id in seen_model_ids:
continue
seen_model_ids.add(info.id)
result.append(info)
# 只有无限制的情况才写入缓存
if api_formats and use_cache:
await _set_cached_models(api_formats, result)
if normalized_formats and use_cache:
await _set_cached_models(normalized_formats, result, client_format)
return result
@@ -415,6 +550,7 @@ def find_model_by_id(
available_provider_ids: set[str],
api_formats: Optional[list[str]] = None,
restrictions: Optional[AccessRestrictions] = None,
provider_to_formats: Optional[dict[str, set[str]]] = None,
) -> Optional[ModelInfo]:
"""
按 ID 查找模型(仅支持 GlobalModel.name
@@ -425,6 +561,7 @@ def find_model_by_id(
available_provider_ids: 有可用端点的 Provider ID 集合
api_formats: API 格式列表,用于检查 Key 的 allowed_models
restrictions: API Key/User 的访问限制
provider_to_formats: Provider -> formats 映射(兼容转换过滤用)
Returns:
ModelInfo 或 None
@@ -432,10 +569,14 @@ def find_model_by_id(
if not available_provider_ids:
return None
normalized_formats = _normalize_api_formats(api_formats, provider_to_formats)
# 如果提供了 api_formats获取真正可用的模型 ID
available_model_ids: Optional[set[str]] = None
if api_formats:
available_model_ids = _get_available_model_ids_for_format(db, api_formats)
if normalized_formats:
available_model_ids = _get_available_model_ids_for_format(
db, normalized_formats, provider_to_formats
)
# 快速检查:如果目标模型不在可用列表中,直接返回 None
if available_model_ids is not None and model_id not in available_model_ids:
return None