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:
fawney19
2026-02-01 17:28:00 +08:00
parent c246ccfc91
commit 7b66505634
219 changed files with 4732 additions and 2545 deletions

View File

@@ -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

View File

@@ -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与格式适配器。"""

View File

@@ -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

View File

@@ -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

View File

@@ -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密钥")