mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 全局适配 ApiFamily/EndpointKind 结构化标识体系
将新的 (ApiFamily, EndpointKind) / `family:kind` 签名体系应用到整个代码库: - API Handlers: 所有 adapter/handler 使用新的签名格式 - Services: provider, model, usage, cache, auth 等服务层适配 - Database: ProviderEndpoint 新增 api_family/endpoint_kind 字段 - Frontend: Provider 管理、Usage 表格等组件适配 - Tests: 更新所有相关测试用例
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src.api.base.context import ApiRequestContext
|
||||
|
||||
from .adapter import ApiAdapter, ApiMode
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
@@ -13,7 +14,6 @@ from src.models.database import ApiKey, ManagementToken, User
|
||||
from src.utils.request_utils import get_client_ip
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApiRequestContext:
|
||||
"""统一的API请求上下文,贯穿Pipeline与格式适配器。"""
|
||||
|
||||
@@ -11,17 +11,20 @@
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import tuple_
|
||||
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.cache_service import CacheService
|
||||
from src.core.logger import logger
|
||||
from src.services.model.availability import ModelAvailabilityQuery
|
||||
from src.models.database import ApiKey, Model, Provider, ProviderEndpoint, User
|
||||
from src.services.model.availability import ModelAvailabilityQuery
|
||||
from src.services.provider.format import normalize_endpoint_signature
|
||||
|
||||
# 缓存 key 前缀
|
||||
_CACHE_KEY_PREFIX = "models:list"
|
||||
@@ -60,7 +63,9 @@ async def _set_cached_models(
|
||||
try:
|
||||
data = [asdict(m) for m in models]
|
||||
await CacheService.set(cache_key, data, ttl_seconds=_CACHE_TTL)
|
||||
logger.debug(f"[ModelsService] 已缓存: {cache_key}, {len(models)} 个模型, TTL={_CACHE_TTL}s")
|
||||
logger.debug(
|
||||
f"[ModelsService] 已缓存: {cache_key}, {len(models)} 个模型, TTL={_CACHE_TTL}s"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[ModelsService] 缓存写入失败: {e}")
|
||||
|
||||
@@ -119,9 +124,7 @@ class AccessRestrictions:
|
||||
allowed_api_formats: list[str] | None = None # 允许的 API 格式列表
|
||||
|
||||
@classmethod
|
||||
def from_api_key_and_user(
|
||||
cls, api_key: ApiKey | None, user: User | None
|
||||
) -> AccessRestrictions:
|
||||
def from_api_key_and_user(cls, api_key: ApiKey | None, user: User | None) -> AccessRestrictions:
|
||||
"""
|
||||
从 API Key 和 User 合并访问限制
|
||||
|
||||
@@ -164,14 +167,16 @@ class AccessRestrictions:
|
||||
检查 API 格式是否被允许
|
||||
|
||||
Args:
|
||||
api_format: API 格式 (如 "OPENAI", "CLAUDE", "GEMINI")
|
||||
api_format: endpoint signature(如 "openai:chat")
|
||||
|
||||
Returns:
|
||||
True 如果格式被允许,False 否则
|
||||
"""
|
||||
if self.allowed_api_formats is None:
|
||||
return True
|
||||
return api_format in self.allowed_api_formats
|
||||
target = normalize_endpoint_signature(api_format)
|
||||
allowed = {normalize_endpoint_signature(f) for f in self.allowed_api_formats if f}
|
||||
return target in allowed
|
||||
|
||||
def is_model_allowed(self, model_id: str, provider_id: str) -> bool:
|
||||
"""
|
||||
@@ -201,14 +206,14 @@ def _normalize_api_formats(
|
||||
api_formats: list[str] | None,
|
||||
provider_to_formats: dict[str, set[str]] | None = None,
|
||||
) -> list[str]:
|
||||
"""规范化 API 格式列表(大写),必要时从 provider_to_formats 兜底"""
|
||||
"""规范化 API 格式列表(endpoint signature,小写 canonical),必要时从 provider_to_formats 兜底"""
|
||||
if api_formats:
|
||||
return [str(fmt).upper() for fmt in api_formats if fmt is not None]
|
||||
return [normalize_endpoint_signature(str(fmt)) for fmt in api_formats if fmt]
|
||||
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)
|
||||
all_formats.update(normalize_endpoint_signature(str(fmt)) for fmt in formats if fmt)
|
||||
return list(all_formats)
|
||||
|
||||
|
||||
@@ -226,7 +231,9 @@ def _get_provider_model_names_for_formats(
|
||||
if not isinstance(raw_mappings, list):
|
||||
return names
|
||||
|
||||
usable_formats_upper = {f.upper() for f in usable_formats} if usable_formats else None
|
||||
usable_formats_norm = (
|
||||
{normalize_endpoint_signature(f) for f in usable_formats} if usable_formats else None
|
||||
)
|
||||
|
||||
for raw in raw_mappings:
|
||||
if not isinstance(raw, dict):
|
||||
@@ -236,15 +243,12 @@ def _get_provider_model_names_for_formats(
|
||||
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
|
||||
if usable_formats_norm and mapping_api_formats and isinstance(mapping_api_formats, list):
|
||||
mapping_formats = {
|
||||
normalize_endpoint_signature(str(fmt)) for fmt in mapping_api_formats if fmt
|
||||
}
|
||||
if not mapping_formats & usable_formats_norm:
|
||||
continue
|
||||
|
||||
names.add(name.strip())
|
||||
|
||||
@@ -266,39 +270,52 @@ def get_compatible_provider_formats(
|
||||
if not normalized_formats:
|
||||
return {}
|
||||
|
||||
target_formats = set(normalized_formats)
|
||||
client_format_upper = client_format.upper()
|
||||
target_pairs: list[tuple[str, str]] = []
|
||||
for fmt in normalized_formats:
|
||||
try:
|
||||
fam, kind = fmt.split(":", 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if fam and kind:
|
||||
target_pairs.append((fam, kind))
|
||||
if not target_pairs:
|
||||
return {}
|
||||
|
||||
client_format_norm = normalize_endpoint_signature(client_format)
|
||||
|
||||
endpoint_rows = (
|
||||
db.query(
|
||||
ProviderEndpoint.provider_id,
|
||||
ProviderEndpoint.api_format,
|
||||
ProviderEndpoint.api_family,
|
||||
ProviderEndpoint.endpoint_kind,
|
||||
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)),
|
||||
ProviderEndpoint.api_family.isnot(None),
|
||||
ProviderEndpoint.endpoint_kind.isnot(None),
|
||||
tuple_(ProviderEndpoint.api_family, ProviderEndpoint.endpoint_kind).in_(target_pairs),
|
||||
)
|
||||
.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:
|
||||
for provider_id, api_family, endpoint_kind, format_acceptance_config in endpoint_rows:
|
||||
if not provider_id or not api_family or not endpoint_kind:
|
||||
continue
|
||||
endpoint_format = normalize_endpoint_signature(f"{api_family}:{endpoint_kind}")
|
||||
is_compatible, _needs_conversion, _reason = is_format_compatible(
|
||||
client_format_upper,
|
||||
str(endpoint_format),
|
||||
client_format_norm,
|
||||
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)
|
||||
provider_to_formats.setdefault(provider_id, set()).add(endpoint_format)
|
||||
|
||||
return provider_to_formats
|
||||
|
||||
@@ -420,7 +437,9 @@ def _extract_model_info(model: Any) -> ModelInfo | None:
|
||||
"""
|
||||
global_model = model.global_model
|
||||
if global_model is None:
|
||||
logger.warning(f"[ModelService] Model {getattr(model, 'id', 'unknown')} 缺少 global_model,跳过")
|
||||
logger.warning(
|
||||
f"[ModelService] Model {getattr(model, 'id', 'unknown')} 缺少 global_model,跳过"
|
||||
)
|
||||
return None
|
||||
|
||||
model_id: str = global_model.name
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any, TypeVar
|
||||
from collections.abc import Sequence
|
||||
|
||||
from sqlalchemy.orm import Query
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ class ApiRequestPipeline:
|
||||
logger.debug("[Pipeline] START | path=%s", http_request.url.path)
|
||||
logger.debug(
|
||||
"[Pipeline] Running with mode=%s, adapter=%s, adapter.mode=%s, path=%s",
|
||||
mode, adapter.__class__.__name__, adapter.mode, http_request.url.path
|
||||
mode,
|
||||
adapter.__class__.__name__,
|
||||
adapter.mode,
|
||||
http_request.url.path,
|
||||
)
|
||||
if mode == ApiMode.ADMIN:
|
||||
user, management_token = await self._authenticate_admin(http_request, db)
|
||||
@@ -94,7 +97,10 @@ class ApiRequestPipeline:
|
||||
http_request.body(), timeout=config.request_body_timeout
|
||||
)
|
||||
if not is_quiet:
|
||||
logger.debug("[Pipeline] Raw body读取完成 | size=%d bytes", len(raw_body) if raw_body is not None else 0)
|
||||
logger.debug(
|
||||
"[Pipeline] Raw body读取完成 | size=%d bytes",
|
||||
len(raw_body) if raw_body is not None else 0,
|
||||
)
|
||||
except TimeoutError:
|
||||
timeout_sec = int(config.request_body_timeout)
|
||||
logger.error(f"读取请求体超时({timeout_sec}s),可能客户端未发送完整请求体")
|
||||
@@ -122,14 +128,22 @@ class ApiRequestPipeline:
|
||||
# 存储 quiet 标志到 context,用于审计日志判断
|
||||
context.quiet_logging = is_quiet
|
||||
if not is_quiet:
|
||||
logger.debug("[Pipeline] Context构建完成 | adapter=%s | request_id=%s", adapter.name, context.request_id)
|
||||
logger.debug(
|
||||
"[Pipeline] Context构建完成 | adapter=%s | request_id=%s",
|
||||
adapter.name,
|
||||
context.request_id,
|
||||
)
|
||||
|
||||
if mode != ApiMode.ADMIN and user:
|
||||
context.quota_remaining = self._calculate_quota_remaining(user)
|
||||
|
||||
if not is_quiet:
|
||||
logger.debug("[Pipeline] Adapter=%s | RequestID=%s", adapter.name, context.request_id)
|
||||
logger.debug("[Pipeline] Calling authorize on %s, user=%s", adapter.__class__.__name__, context.user)
|
||||
logger.debug(
|
||||
"[Pipeline] Calling authorize on %s, user=%s",
|
||||
adapter.__class__.__name__,
|
||||
context.user,
|
||||
)
|
||||
# authorize 可能是异步的,需要检查并 await
|
||||
authorize_result = adapter.authorize(context)
|
||||
if hasattr(authorize_result, "__await__"):
|
||||
@@ -172,7 +186,10 @@ class ApiRequestPipeline:
|
||||
# 使用 adapter 的 extract_api_key 方法,支持不同 API 格式的认证头
|
||||
client_api_key = adapter.extract_api_key(request)
|
||||
if not quiet:
|
||||
logger.debug("[Pipeline._authenticate_client] 提取API密钥完成 | key_prefix=%s...", client_api_key[:8] if client_api_key else None)
|
||||
logger.debug(
|
||||
"[Pipeline._authenticate_client] 提取API密钥完成 | key_prefix=%s...",
|
||||
client_api_key[:8] if client_api_key else None,
|
||||
)
|
||||
if not client_api_key:
|
||||
raise HTTPException(status_code=401, detail="请提供API密钥")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user