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

@@ -10,14 +10,12 @@ from typing import Any
from sqlalchemy.orm import Session
from src.core.api_format import APIFormat
from src.core.exceptions import ProviderNotAvailableException
from src.core.logger import logger
from src.models.database import ApiKey
from src.services.cache.aware_scheduler import CacheAwareScheduler, ProviderCandidate
class CandidateResolver:
"""
候选解析器 - 负责获取和排序可用的 Provider 组合
@@ -45,7 +43,7 @@ class CandidateResolver:
async def fetch_candidates(
self,
api_format: APIFormat,
api_format: str,
model_name: str,
affinity_key: str,
user_api_key: ApiKey | None = None,
@@ -78,6 +76,12 @@ class CandidateResolver:
provider_batch_size = 20
global_model_id: str | None = None
logger.debug(
"[CandidateResolver] fetch_candidates starting: model=%s, api_format=%s",
model_name,
api_format,
)
while True:
candidates, resolved_global_model_id = await self.cache_scheduler.list_all_candidates(
db=self.db,
@@ -91,6 +95,12 @@ class CandidateResolver:
capability_requirements=capability_requirements,
)
logger.debug(
"[CandidateResolver] list_all_candidates batch: offset=%d, returned=%d candidates",
provider_offset,
len(candidates),
)
if resolved_global_model_id and global_model_id is None:
global_model_id = resolved_global_model_id
@@ -100,6 +110,11 @@ class CandidateResolver:
all_candidates.extend(candidates)
provider_offset += provider_batch_size
logger.debug(
"[CandidateResolver] fetch_candidates completed: total=%d candidates",
len(all_candidates),
)
if not all_candidates:
logger.error(f" [{request_id}] 没有找到任何可用的 Provider/Endpoint/Key 组合")
request_type = "流式" if is_stream else "非流式"
@@ -196,7 +211,9 @@ class CandidateResolver:
candidate_record_map[(candidate_index, 0)] = record_id
else:
# max_retries 已从 Endpoint 迁移到 ProviderEndpoint 仍可能保留旧字段用于兼容)
max_retries_for_candidate = int(provider.max_retries or 2) if candidate.is_cached else 1
max_retries_for_candidate = (
int(provider.max_retries or 2) if candidate.is_cached else 1
)
for retry_index in range(max_retries_for_candidate):
record_id = str(uuid.uuid4())
@@ -226,7 +243,9 @@ class CandidateResolver:
)
self.db.flush()
logger.debug(f" [{request_id}] 批量插入完成: {len(candidate_records_to_insert)} 条记录")
logger.debug(
f" [{request_id}] 批量插入完成: {len(candidate_records_to_insert)} 条记录"
)
return candidate_record_map

View File

@@ -13,7 +13,7 @@ from typing import Any
import httpx
from sqlalchemy.orm import Session
from src.core.api_format import APIFormat
from src.core.api_format.signature import make_signature_key
from src.core.exceptions import (
ConcurrencyLimitError,
ProviderAuthException,
@@ -28,7 +28,7 @@ from src.core.logger import logger
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
from src.services.cache.aware_scheduler import CacheAwareScheduler
from src.services.health.monitor import health_monitor
from src.services.provider.format import normalize_api_format
from src.services.provider.format import normalize_endpoint_signature
from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
from src.services.rate_limit.detector import RateLimitType, detect_rate_limit_type
@@ -561,7 +561,7 @@ class ErrorClassifier:
endpoint: ProviderEndpoint,
key: ProviderAPIKey,
affinity_key: str,
api_format: str | APIFormat,
api_format: str,
global_model_id: str,
request_id: str | None,
captured_key_concurrent: int | None,
@@ -618,18 +618,11 @@ class ErrorClassifier:
extra_data["error_response"] = error_response_text
# client_format用于缓存亲和性/缓存失效(用户视角)
client_format_str = (
normalize_api_format(api_format).value
if isinstance(api_format, (str, APIFormat))
else str(api_format)
)
client_format_str = normalize_endpoint_signature(api_format)
# provider_format用于健康度/熔断 bucketProvider 真实端点格式)
provider_api_format = getattr(endpoint, "api_format", None)
provider_format_str = (
provider_api_format.value
if isinstance(provider_api_format, APIFormat)
else str(provider_api_format or client_format_str)
).upper()
fam = str(getattr(endpoint, "api_family", "")).strip().lower()
kind = str(getattr(endpoint, "endpoint_kind", "")).strip().lower()
provider_format_str = make_signature_key(fam, kind) if fam and kind else client_format_str
# 处理客户端请求错误(不应重试,不失效缓存,不记录健康失败)
if isinstance(converted_error, UpstreamClientException):
@@ -704,7 +697,7 @@ class ErrorClassifier:
endpoint: ProviderEndpoint,
key: ProviderAPIKey,
affinity_key: str,
api_format: str | APIFormat,
api_format: str,
global_model_id: str,
captured_key_concurrent: int | None,
elapsed_ms: int | None,
@@ -737,18 +730,11 @@ class ErrorClassifier:
)
# client_format用于缓存亲和性/缓存失效(用户视角)
client_format_str = (
normalize_api_format(api_format).value
if isinstance(api_format, (str, APIFormat))
else str(api_format)
)
client_format_str = normalize_endpoint_signature(api_format)
# provider_format用于健康度/熔断 bucketProvider 真实端点格式)
provider_api_format = getattr(endpoint, "api_format", None)
provider_format_str = (
provider_api_format.value
if isinstance(provider_api_format, APIFormat)
else str(provider_api_format or client_format_str)
).upper()
fam = str(getattr(endpoint, "api_family", "")).strip().lower()
kind = str(getattr(endpoint, "endpoint_kind", "")).strip().lower()
provider_format_str = make_signature_key(fam, kind) if fam and kind else client_format_str
# 处理限流错误
if isinstance(error, ProviderRateLimitException) and key:

View File

@@ -21,19 +21,16 @@
- 本类作为协调者,组合使用上述组件
"""
from __future__ import annotations
from typing import Any, NoReturn
from collections.abc import Callable
from typing import Any, NoReturn
import httpx
from redis import Redis
from sqlalchemy.orm import Session
from src.config.settings import config
from src.core.api_format import APIFormat
from src.core.api_format.conversion.exceptions import FormatConversionError
from src.core.error_utils import extract_error_message
from src.core.exceptions import (
@@ -51,7 +48,7 @@ from src.services.cache.aware_scheduler import (
get_cache_aware_scheduler,
)
from src.services.message.thinking_rectifier import ThinkingRectifier
from src.services.provider.format import normalize_api_format
from src.services.provider.format import normalize_endpoint_signature
from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
from src.services.rate_limit.concurrency_manager import get_concurrency_manager
from src.services.request.candidate import RequestCandidateService
@@ -172,7 +169,7 @@ class FallbackOrchestrator:
async def _fetch_all_candidates(
self,
api_format: APIFormat,
api_format: str,
model_name: str,
affinity_key: str,
user_api_key: ApiKey | None = None,
@@ -256,7 +253,7 @@ class FallbackOrchestrator:
user_api_key: ApiKey,
request_func: Callable[..., Any],
request_id: str | None,
api_format: APIFormat,
api_format: str,
model_name: str,
affinity_key: str,
global_model_id: str,
@@ -343,8 +340,11 @@ class FallbackOrchestrator:
if not config.thinking_rectifier_enabled:
logger.info(f" [{request_id}] Thinking 错误:整流器已禁用,终止重试")
self._mark_thinking_error_failed(
candidate_record_id, converted_error, elapsed_ms,
captured_key_concurrent, serializable_extra_data
candidate_record_id,
converted_error,
elapsed_ms,
captured_key_concurrent,
serializable_extra_data,
)
raise converted_error
@@ -352,8 +352,11 @@ class FallbackOrchestrator:
if request_body_ref is None:
logger.warning(f" [{request_id}] Thinking 错误:无法获取请求体引用,终止重试")
self._mark_thinking_error_failed(
candidate_record_id, converted_error, elapsed_ms,
captured_key_concurrent, serializable_extra_data
candidate_record_id,
converted_error,
elapsed_ms,
captured_key_concurrent,
serializable_extra_data,
)
raise converted_error
@@ -361,8 +364,11 @@ class FallbackOrchestrator:
if request_body_ref.get("_rectified", False):
logger.warning(f" [{request_id}] Thinking 错误:已整流仍失败,终止重试")
self._mark_thinking_error_failed(
candidate_record_id, converted_error, elapsed_ms,
captured_key_concurrent, {**serializable_extra_data, "rectified": True}
candidate_record_id,
converted_error,
elapsed_ms,
captured_key_concurrent,
{**serializable_extra_data, "rectified": True},
)
raise converted_error
@@ -383,8 +389,11 @@ class FallbackOrchestrator:
# 标记当前尝试为失败(整流前的状态)
# 注意:整流后重试会复用此记录 ID成功时会覆盖为 success 状态
self._mark_thinking_error_failed(
candidate_record_id, converted_error, elapsed_ms,
captured_key_concurrent, {**serializable_extra_data, "rectified": True}
candidate_record_id,
converted_error,
elapsed_ms,
captured_key_concurrent,
{**serializable_extra_data, "rectified": True},
)
# 返回 continue在当前候选的重试循环中继续使用整流后的请求体重试
@@ -392,8 +401,11 @@ class FallbackOrchestrator:
else:
logger.warning(f" [{request_id}] Thinking 错误:无可整流内容")
self._mark_thinking_error_failed(
candidate_record_id, converted_error, elapsed_ms,
captured_key_concurrent, serializable_extra_data
candidate_record_id,
converted_error,
elapsed_ms,
captured_key_concurrent,
serializable_extra_data,
)
raise converted_error
@@ -425,7 +437,7 @@ class FallbackOrchestrator:
retry_index: int,
max_retries_for_candidate: int,
affinity_key: str,
api_format: APIFormat,
api_format: str,
global_model_id: str,
request_id: str | None,
attempt: int,
@@ -506,9 +518,7 @@ class FallbackOrchestrator:
"provider_id": str(provider.id),
"provider_endpoint_id": str(endpoint.id),
"provider_api_key_id": str(key.id),
"api_format": (
api_format.value if hasattr(api_format, "value") else str(api_format)
),
"api_format": str(api_format),
}
raise client_error
else:
@@ -588,9 +598,7 @@ class FallbackOrchestrator:
"provider_id": str(provider.id),
"provider_endpoint_id": str(endpoint.id),
"provider_api_key_id": str(key.id),
"api_format": (
api_format.value if hasattr(api_format, "value") else str(api_format)
),
"api_format": str(api_format),
}
raise converted_error
@@ -662,7 +670,7 @@ class FallbackOrchestrator:
user_api_key: ApiKey,
model_name: str,
is_stream: bool,
api_format_enum: APIFormat,
api_format: str,
) -> None:
"""创建 pending 状态的使用记录(用于实时状态追踪)"""
if not request_id:
@@ -681,7 +689,7 @@ class FallbackOrchestrator:
api_key=user_api_key,
model=model_name,
is_stream=is_stream,
api_format=api_format_enum.value,
api_format=api_format,
)
except Exception as e:
# 创建 pending 记录失败不应阻塞请求
@@ -694,7 +702,7 @@ class FallbackOrchestrator:
user_api_key: ApiKey,
request_func: Callable[..., Any],
request_id: str | None,
api_format_enum: APIFormat,
api_format: str,
model_name: str,
affinity_key: str,
global_model_id: str,
@@ -724,7 +732,7 @@ class FallbackOrchestrator:
user_api_key=user_api_key,
request_func=request_func,
request_id=request_id,
api_format_enum=api_format_enum,
api_format=api_format,
model_name=model_name,
affinity_key=affinity_key,
global_model_id=global_model_id,
@@ -735,9 +743,9 @@ class FallbackOrchestrator:
)
if result["success"]:
response: tuple[
Any, str, str | None, str | None, str | None, str | None
] = result["response"]
response: tuple[Any, str, str | None, str | None, str | None, str | None] = result[
"response"
]
return response
# 更新计数器和错误信息
@@ -746,14 +754,12 @@ class FallbackOrchestrator:
if result.get("error"):
last_error = result["error"]
if result.get("should_raise") and last_error is not None:
self._attach_metadata_to_error(
last_error, last_candidate, model_name, api_format_enum
)
self._attach_metadata_to_error(last_error, last_candidate, model_name, api_format)
raise last_error
# 所有组合都已尝试完毕,全部失败
self._raise_all_failed_exception(
request_id, max_attempts, last_candidate, model_name, api_format_enum, last_error
request_id, max_attempts, last_candidate, model_name, api_format, last_error
)
async def _try_candidate_with_retries(
@@ -764,7 +770,7 @@ class FallbackOrchestrator:
user_api_key: ApiKey,
request_func: Callable[..., Any],
request_id: str | None,
api_format_enum: APIFormat,
api_format: str,
model_name: str,
affinity_key: str,
global_model_id: str,
@@ -820,7 +826,7 @@ class FallbackOrchestrator:
user_api_key=user_api_key,
request_func=request_func,
request_id=request_id,
api_format=api_format_enum,
api_format=api_format,
model_name=model_name,
affinity_key=affinity_key,
global_model_id=global_model_id,
@@ -839,7 +845,7 @@ class FallbackOrchestrator:
retry_index=retry_index,
max_retries_for_candidate=max_retries_for_candidate,
affinity_key=affinity_key,
api_format=api_format_enum,
api_format=api_format,
global_model_id=global_model_id,
request_id=request_id,
attempt=attempt_counter,
@@ -886,7 +892,7 @@ class FallbackOrchestrator:
error: Exception | None,
candidate: ProviderCandidate | None,
model_name: str,
api_format_enum: APIFormat,
api_format: str,
) -> None:
"""附加 candidate 信息到异常,以便记录 usage"""
if not error or not candidate:
@@ -915,7 +921,7 @@ class FallbackOrchestrator:
provider_api_key_id=(
getattr(existing_metadata, "provider_api_key_id", None) or str(candidate.key.id)
),
api_format=api_format_enum.value,
api_format=api_format,
)
# 使用 setattr 避免类型检查错误
setattr(error, "request_metadata", metadata)
@@ -926,7 +932,7 @@ class FallbackOrchestrator:
max_attempts: int,
last_candidate: ProviderCandidate | None,
model_name: str,
api_format_enum: APIFormat,
api_format: str,
last_error: Exception | None = None,
) -> NoReturn:
"""所有组合都失败时抛出异常"""
@@ -940,7 +946,7 @@ class FallbackOrchestrator:
"provider_id": str(last_candidate.provider.id),
"provider_endpoint_id": str(last_candidate.endpoint.id),
"provider_api_key_id": str(last_candidate.key.id),
"api_format": api_format_enum.value,
"api_format": api_format,
}
# 提取上游错误响应
@@ -988,7 +994,7 @@ class FallbackOrchestrator:
async def execute_with_fallback(
self,
api_format: str | APIFormat,
api_format: str,
model_name: str,
user_api_key: ApiKey,
request_func: Callable[[Provider, ProviderEndpoint, ProviderAPIKey], Any],
@@ -1002,7 +1008,7 @@ class FallbackOrchestrator:
执行请求,并在失败时自动故障转移(缓存感知)
Args:
api_format: API 格式(如 'CLAUDE', 'OPENAI'
api_format: endpoint signature'claude:chat', 'openai:cli'
model_name: 模型名称
user_api_key: 用户的 API Key对象
request_func: 请求函数,接收 (provider, endpoint, key) 参数,返回响应
@@ -1023,22 +1029,22 @@ class FallbackOrchestrator:
# 准备执行上下文
affinity_key = str(user_api_key.id)
user_id = str(user_api_key.user_id)
api_format_enum = normalize_api_format(api_format)
api_format_norm = normalize_endpoint_signature(api_format)
logger.debug(
f"[FallbackOrchestrator] execute_with_fallback 被调用: "
f"api_format={api_format_enum.value}, model_name={model_name}, "
f"api_format={api_format_norm}, model_name={model_name}, "
f"request_id={request_id}, is_stream={is_stream}"
)
# 创建 pending 状态的使用记录
self._create_pending_usage_record(
request_id, user_api_key, model_name, is_stream, api_format_enum
request_id, user_api_key, model_name, is_stream, api_format_norm
)
# 1. 收集所有候选(同时获取规范化的 global_model_id 用于缓存亲和性)
all_candidates, global_model_id = await self._fetch_all_candidates(
api_format=api_format_enum,
api_format=api_format_norm,
model_name=model_name,
affinity_key=affinity_key,
user_api_key=user_api_key,
@@ -1064,7 +1070,7 @@ class FallbackOrchestrator:
user_api_key=user_api_key,
request_func=request_func,
request_id=request_id,
api_format_enum=api_format_enum,
api_format=api_format_norm,
model_name=model_name,
affinity_key=affinity_key,
global_model_id=global_model_id,

View File

@@ -4,13 +4,11 @@
负责执行单个候选请求
"""
from typing import Any
from collections.abc import Callable
from typing import Any
from sqlalchemy.orm import Session
from src.core.api_format import APIFormat
from src.core.logger import logger
from src.models.database import ApiKey
from src.services.cache.aware_scheduler import CacheAwareScheduler, ProviderCandidate
@@ -18,7 +16,6 @@ from src.services.request.candidate import RequestCandidateService
from src.services.request.executor import RequestExecutor
class RequestDispatcher:
"""
请求分发器 - 负责执行单个候选请求
@@ -56,7 +53,7 @@ class RequestDispatcher:
user_api_key: ApiKey,
request_func: Callable[..., Any],
request_id: str | None,
api_format: APIFormat,
api_format: str,
model_name: str,
affinity_key: str,
global_model_id: str,
@@ -133,15 +130,12 @@ class RequestDispatcher:
# 设置缓存亲和性
if provider_supports_caching and self.cache_scheduler is not None:
try:
api_format_str = (
api_format.value if isinstance(api_format, APIFormat) else api_format
)
await self.cache_scheduler.set_cache_affinity(
affinity_key=affinity_key,
provider_id=provider_id,
endpoint_id=endpoint_id,
key_id=key_id,
api_format=api_format_str,
api_format=api_format,
global_model_id=global_model_id,
ttl=provider_cache_ttl_seconds,
)