mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
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:
@@ -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_format(Key 直属 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
|
||||
|
||||
Reference in New Issue
Block a user