mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 实现跨 API 格式自动转换功能
- 新增端点级 format_acceptance_config 配置,控制是否接受跨格式请求 - 重构 EndpointFormDialog 为卡片式布局,支持内联编辑和格式转换开关 - StreamProcessor 实现流式响应的跨格式转换,支持 OpenAI/Claude/Gemini 互转 - CacheAwareScheduler 按端点格式筛选候选,同格式优先于跨格式 - 健康度/熔断按 Provider 端点格式分桶,而非客户端请求格式 - 新增 format_conversion_total 和 format_conversion_duration_seconds 指标 - 新增全局配置 format_conversion_enabled 控制总开关 - Input 组件新增 size="sm" 尺寸选项
This commit is contained in:
244
src/services/cache/aware_scheduler.py
vendored
244
src/services/cache/aware_scheduler.py
vendored
@@ -35,7 +35,7 @@ import random
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
@@ -65,6 +65,7 @@ from src.services.rate_limit.adaptive_reservation import (
|
||||
get_adaptive_reservation_manager,
|
||||
)
|
||||
from src.services.rate_limit.concurrency_manager import get_concurrency_manager
|
||||
from src.services.system.config import SystemConfigService
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -78,6 +79,8 @@ class ProviderCandidate:
|
||||
is_skipped: bool = False # 是否被跳过
|
||||
skip_reason: Optional[str] = None # 跳过原因
|
||||
mapping_matched_model: Optional[str] = None # 通过映射匹配到的模型名(用于实际请求)
|
||||
needs_conversion: bool = False # 是否需要格式转换
|
||||
provider_api_format: str = "" # Provider 端点实际格式(用于健康度/熔断 bucket)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -129,7 +132,10 @@ class CacheAwareScheduler:
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, redis_client=None, priority_mode: Optional[str] = None, scheduling_mode: Optional[str] = None
|
||||
self,
|
||||
redis_client=None,
|
||||
priority_mode: Optional[str] = None,
|
||||
scheduling_mode: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
初始化调度器
|
||||
@@ -149,7 +155,9 @@ class CacheAwareScheduler:
|
||||
self.scheduling_mode = self._normalize_scheduling_mode(
|
||||
scheduling_mode or self.SCHEDULING_MODE_CACHE_AFFINITY
|
||||
)
|
||||
logger.debug(f"[CacheAwareScheduler] 初始化优先级模式: {self.priority_mode}, 调度模式: {self.scheduling_mode}")
|
||||
logger.debug(
|
||||
f"[CacheAwareScheduler] 初始化优先级模式: {self.priority_mode}, 调度模式: {self.scheduling_mode}"
|
||||
)
|
||||
|
||||
# 初始化子组件(将在第一次使用时异步初始化)
|
||||
self._affinity_manager: Optional[CacheAffinityManager] = None
|
||||
@@ -429,7 +437,9 @@ class CacheAwareScheduler:
|
||||
import math
|
||||
|
||||
# 与 ConcurrencyManager 的 Lua 脚本保持一致:使用 floor 计算新用户可用槽位
|
||||
available_for_new = max(1, math.floor(effective_key_limit * (1 - reservation_ratio)))
|
||||
available_for_new = max(
|
||||
1, math.floor(effective_key_limit * (1 - reservation_ratio))
|
||||
)
|
||||
if key_count >= available_for_new:
|
||||
logger.debug(
|
||||
f"Key {key.id[:8]}... 新用户配额已满 "
|
||||
@@ -531,8 +541,7 @@ class CacheAwareScheduler:
|
||||
|
||||
# 合并 allowed_api_formats
|
||||
result["allowed_api_formats"] = merge_restrictions(
|
||||
user_api_key.allowed_api_formats,
|
||||
user.allowed_api_formats if user else None
|
||||
user_api_key.allowed_api_formats, user.allowed_api_formats if user else None
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -578,7 +587,9 @@ class CacheAwareScheduler:
|
||||
target_format = normalize_api_format(api_format)
|
||||
|
||||
# 0. 解析 model_name 到 GlobalModel(支持直接匹配和映射名匹配,使用 ModelCacheService)
|
||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(db, model_name)
|
||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(
|
||||
db, model_name
|
||||
)
|
||||
|
||||
if not global_model:
|
||||
logger.warning(f"GlobalModel not found: {model_name}")
|
||||
@@ -592,7 +603,9 @@ class CacheAwareScheduler:
|
||||
# 提取模型映射(用于 Provider Key 的 allowed_models 匹配)
|
||||
model_mappings: List[str] = (global_model.config or {}).get("model_mappings", [])
|
||||
if model_mappings:
|
||||
logger.debug(f"[Scheduler] GlobalModel={global_model.name} 配置了映射规则: {model_mappings}")
|
||||
logger.debug(
|
||||
f"[Scheduler] GlobalModel={global_model.name} 配置了映射规则: {model_mappings}"
|
||||
)
|
||||
|
||||
# 获取合并后的访问限制(ApiKey + User)
|
||||
restrictions = self._get_effective_restrictions(user_api_key)
|
||||
@@ -654,10 +667,13 @@ class CacheAwareScheduler:
|
||||
return [], global_model_id
|
||||
|
||||
# 2. 构建候选列表(传入 is_stream 和 capability_requirements 用于过滤)
|
||||
global_conversion_enabled = bool(
|
||||
SystemConfigService.get_config(db, "format_conversion_enabled", False)
|
||||
)
|
||||
candidates = await self._build_candidates(
|
||||
db=db,
|
||||
providers=providers,
|
||||
target_format=target_format,
|
||||
client_format=target_format,
|
||||
model_name=requested_model_name,
|
||||
resolved_model_name=resolved_model_name,
|
||||
model_mappings=model_mappings,
|
||||
@@ -665,6 +681,7 @@ class CacheAwareScheduler:
|
||||
max_candidates=max_candidates,
|
||||
is_stream=is_stream,
|
||||
capability_requirements=capability_requirements,
|
||||
global_conversion_enabled=global_conversion_enabled,
|
||||
)
|
||||
|
||||
# 3. 应用优先级模式排序
|
||||
@@ -774,15 +791,25 @@ class CacheAwareScheduler:
|
||||
- provider_model_names: Provider 侧可用的模型名称集合(主名称 + 映射名称,按 api_format 过滤)
|
||||
"""
|
||||
# 使用 ModelCacheService 解析模型名称(支持映射名)
|
||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(db, model_name)
|
||||
global_model = await ModelCacheService.resolve_global_model_by_name_or_mapping(
|
||||
db, model_name
|
||||
)
|
||||
|
||||
if not global_model:
|
||||
# 完全未找到匹配
|
||||
return False, "模型不存在或 Provider 未配置此模型", None, None
|
||||
|
||||
# 找到 GlobalModel 后,检查当前 Provider 是否支持
|
||||
is_supported, skip_reason, caps, provider_model_names = await self._check_model_support_for_global_model(
|
||||
db, provider, global_model, model_name, api_format, is_stream, capability_requirements
|
||||
is_supported, skip_reason, caps, provider_model_names = (
|
||||
await self._check_model_support_for_global_model(
|
||||
db,
|
||||
provider,
|
||||
global_model,
|
||||
model_name,
|
||||
api_format,
|
||||
is_stream,
|
||||
capability_requirements,
|
||||
)
|
||||
)
|
||||
return is_supported, skip_reason, caps, provider_model_names
|
||||
|
||||
@@ -814,6 +841,7 @@ class CacheAwareScheduler:
|
||||
# 注意:从缓存重建的对象是 transient 状态,不能使用 load=False
|
||||
# 使用 load=True(默认)允许 SQLAlchemy 正确处理 transient 对象
|
||||
from sqlalchemy import inspect
|
||||
|
||||
insp = inspect(global_model)
|
||||
if insp.transient or insp.detached:
|
||||
# transient/detached 对象:使用默认 merge(会查询 DB 检查是否存在)
|
||||
@@ -940,12 +968,18 @@ class CacheAwareScheduler:
|
||||
return False, f"映射规则无效: {str(e)}", None
|
||||
except Exception as e:
|
||||
# 其他未知异常
|
||||
logger.error(f"映射匹配异常: key_id={key.id}, model={model_name}, error={e}", exc_info=True)
|
||||
logger.error(
|
||||
f"映射匹配异常: key_id={key.id}, model={model_name}, error={e}", exc_info=True
|
||||
)
|
||||
# 异常时保守处理:不允许使用该 Key
|
||||
return False, "映射匹配失败", None
|
||||
|
||||
if not is_allowed:
|
||||
return False, f"模型权限不匹配(允许: {get_allowed_models_preview(key.allowed_models)})", None
|
||||
return (
|
||||
False,
|
||||
f"模型权限不匹配(允许: {get_allowed_models_preview(key.allowed_models)})",
|
||||
None,
|
||||
)
|
||||
|
||||
# Key 级别的能力匹配检查
|
||||
# 注意:模型级别的能力检查已在 _check_model_support 中完成
|
||||
@@ -964,7 +998,7 @@ class CacheAwareScheduler:
|
||||
self,
|
||||
db: Session,
|
||||
providers: List[Provider],
|
||||
target_format: APIFormat,
|
||||
client_format: APIFormat,
|
||||
model_name: str,
|
||||
affinity_key: Optional[str],
|
||||
resolved_model_name: Optional[str] = None,
|
||||
@@ -972,16 +1006,17 @@ class CacheAwareScheduler:
|
||||
max_candidates: Optional[int] = None,
|
||||
is_stream: bool = False,
|
||||
capability_requirements: Optional[Dict[str, bool]] = None,
|
||||
global_conversion_enabled: bool = False,
|
||||
) -> List[ProviderCandidate]:
|
||||
"""
|
||||
构建候选列表
|
||||
|
||||
Key 直属 Provider,通过 api_formats 筛选符合目标格式的 Key。
|
||||
Key 直属 Provider,通过 api_formats 筛选符合端点格式的 Key。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
providers: Provider 列表
|
||||
target_format: 目标 API 格式
|
||||
client_format: 客户端请求的 API 格式
|
||||
model_name: 模型名称(用户请求的名称,可能是映射名)
|
||||
affinity_key: 亲和性标识符(通常为API Key ID)
|
||||
resolved_model_name: 解析后的 GlobalModel.name(用于 Key.allowed_models 校验)
|
||||
@@ -989,89 +1024,122 @@ class CacheAwareScheduler:
|
||||
max_candidates: 最大候选数
|
||||
is_stream: 是否是流式请求,如果为 True 则过滤不支持流式的 Provider
|
||||
capability_requirements: 能力需求(可选)
|
||||
global_conversion_enabled: 全局格式转换开关
|
||||
|
||||
Returns:
|
||||
候选列表
|
||||
"""
|
||||
from src.core.api_format.conversion.compatibility import is_format_compatible
|
||||
|
||||
candidates: List[ProviderCandidate] = []
|
||||
target_format_str = target_format.value
|
||||
client_format_str = client_format.value
|
||||
|
||||
for provider in providers:
|
||||
# 检查模型支持(同时检查流式支持和模型能力需求)
|
||||
supports_model, skip_reason, _model_caps, provider_model_names = await self._check_model_support(
|
||||
db,
|
||||
provider,
|
||||
model_name,
|
||||
api_format=target_format_str,
|
||||
is_stream=is_stream,
|
||||
capability_requirements=capability_requirements,
|
||||
)
|
||||
if not supports_model:
|
||||
logger.debug(f"Provider {provider.name} 不支持模型 {model_name}: {skip_reason}")
|
||||
continue
|
||||
# 按端点格式分别判断兼容性与模型/Key 可用性:
|
||||
# - 同格式端点优先(needs_conversion=False)
|
||||
# - 跨格式端点次之(needs_conversion=True)
|
||||
model_support_cache: Dict[
|
||||
str, Tuple[bool, Optional[str], Optional[List[str]], Optional[Set[str]]]
|
||||
] = {}
|
||||
exact_candidates: List[ProviderCandidate] = []
|
||||
convertible_candidates: List[ProviderCandidate] = []
|
||||
|
||||
# 查找目标格式对应的 Endpoint(获取请求配置)
|
||||
target_endpoint = None
|
||||
for endpoint in provider.endpoints:
|
||||
if not endpoint.is_active:
|
||||
continue
|
||||
|
||||
endpoint_format_str = (
|
||||
endpoint.api_format
|
||||
if isinstance(endpoint.api_format, str)
|
||||
else endpoint.api_format.value
|
||||
)
|
||||
if endpoint.is_active and endpoint_format_str == target_format_str:
|
||||
target_endpoint = endpoint
|
||||
break
|
||||
|
||||
if not target_endpoint:
|
||||
logger.debug(f"Provider {provider.name} 没有活跃的 {target_format_str} 端点")
|
||||
continue
|
||||
|
||||
# Key 直属 Provider,通过 api_formats 筛选
|
||||
active_keys = [
|
||||
key for key in provider.api_keys
|
||||
if key.is_active and target_format_str in (key.api_formats or [])
|
||||
]
|
||||
|
||||
if not active_keys:
|
||||
logger.debug(f"Provider {provider.name} 没有支持 {target_format_str} 的活跃 Key")
|
||||
continue
|
||||
|
||||
# 检查是否所有 Key 都是 TTL=0(轮换模式)
|
||||
use_random = all(
|
||||
(key.cache_ttl_minutes or 0) == 0 for key in active_keys
|
||||
) if active_keys else False
|
||||
if use_random and len(active_keys) > 1:
|
||||
logger.debug(
|
||||
f" Provider {provider.name} 启用 Key 轮换模式 (TTL=0, {len(active_keys)} keys)"
|
||||
is_compatible, needs_conversion, _compat_reason = is_format_compatible(
|
||||
client_format_str,
|
||||
endpoint_format_str,
|
||||
getattr(endpoint, "format_acceptance_config", None),
|
||||
is_stream,
|
||||
global_conversion_enabled,
|
||||
)
|
||||
keys = self._shuffle_keys_by_internal_priority(active_keys, affinity_key, use_random)
|
||||
if not is_compatible:
|
||||
continue
|
||||
|
||||
for key in keys:
|
||||
# Key 级别的能力检查
|
||||
# 注意:不传入 candidate_models 限制,允许映射匹配到 Key 的 allowed_models 中的任意模型名
|
||||
# 这支持以下场景:Key 只允许使用 gpt-5.2,而 GlobalModel 配置了映射 gpt-5.*2
|
||||
# 映射匹配后,实际请求会使用 gpt-5.2 作为模型名发送给 Provider
|
||||
is_available, skip_reason, mapping_matched_model = self._check_key_availability(
|
||||
key,
|
||||
target_format_str,
|
||||
model_name,
|
||||
capability_requirements,
|
||||
resolved_model_name=resolved_model_name,
|
||||
model_mappings=model_mappings,
|
||||
# 检查模型支持(按端点格式过滤 provider_model_mappings)
|
||||
if endpoint_format_str not in model_support_cache:
|
||||
model_support_cache[endpoint_format_str] = await self._check_model_support(
|
||||
db,
|
||||
provider,
|
||||
model_name,
|
||||
api_format=endpoint_format_str,
|
||||
is_stream=is_stream,
|
||||
capability_requirements=capability_requirements,
|
||||
)
|
||||
supports_model, skip_reason, _model_caps, provider_model_names = (
|
||||
model_support_cache[endpoint_format_str]
|
||||
)
|
||||
if not supports_model:
|
||||
logger.debug(
|
||||
f"Provider {provider.name} 端点 {endpoint_format_str} 不支持模型 {model_name}: {skip_reason}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Key 直属 Provider,通过 api_formats 按端点格式筛选
|
||||
active_keys = [
|
||||
key
|
||||
for key in provider.api_keys
|
||||
if key.is_active and endpoint_format_str in (key.api_formats or [])
|
||||
]
|
||||
if not active_keys:
|
||||
continue
|
||||
|
||||
# 检查是否所有 Key 都是 TTL=0(轮换模式)
|
||||
use_random = all((key.cache_ttl_minutes or 0) == 0 for key in active_keys)
|
||||
if use_random and len(active_keys) > 1:
|
||||
logger.debug(
|
||||
f" Provider {provider.name} 启用 Key 轮换模式 "
|
||||
f"(endpoint_format={endpoint_format_str}, {len(active_keys)} keys)"
|
||||
)
|
||||
|
||||
keys = self._shuffle_keys_by_internal_priority(
|
||||
active_keys, affinity_key, use_random
|
||||
)
|
||||
|
||||
candidate = ProviderCandidate(
|
||||
provider=provider,
|
||||
endpoint=target_endpoint,
|
||||
key=key,
|
||||
is_skipped=not is_available,
|
||||
skip_reason=skip_reason,
|
||||
mapping_matched_model=mapping_matched_model,
|
||||
)
|
||||
candidates.append(candidate)
|
||||
for key in keys:
|
||||
# Key 级别检查(健康度/熔断按 provider_format bucket)
|
||||
# 注意:不传入 candidate_models,保持原有映射匹配行为
|
||||
is_available, key_skip_reason, mapping_matched_model = (
|
||||
self._check_key_availability(
|
||||
key,
|
||||
endpoint_format_str,
|
||||
model_name,
|
||||
capability_requirements,
|
||||
resolved_model_name=resolved_model_name,
|
||||
model_mappings=model_mappings,
|
||||
)
|
||||
)
|
||||
|
||||
if max_candidates and len(candidates) >= max_candidates:
|
||||
return candidates
|
||||
candidate = ProviderCandidate(
|
||||
provider=provider,
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
is_skipped=not is_available,
|
||||
skip_reason=key_skip_reason,
|
||||
mapping_matched_model=mapping_matched_model,
|
||||
needs_conversion=needs_conversion,
|
||||
provider_api_format=str(endpoint_format_str or "").upper(),
|
||||
)
|
||||
|
||||
if needs_conversion:
|
||||
convertible_candidates.append(candidate)
|
||||
else:
|
||||
exact_candidates.append(candidate)
|
||||
|
||||
candidates.extend(exact_candidates)
|
||||
candidates.extend(convertible_candidates)
|
||||
|
||||
# max_candidates 截断应在所有候选收集完成后统一处理,确保优先级排序正确
|
||||
if max_candidates and len(candidates) > max_candidates:
|
||||
candidates = candidates[:max_candidates]
|
||||
|
||||
return candidates
|
||||
|
||||
@@ -1173,7 +1241,9 @@ class CacheAwareScheduler:
|
||||
normalized = (mode or "").strip().lower()
|
||||
if normalized not in self.ALLOWED_SCHEDULING_MODES:
|
||||
if normalized:
|
||||
logger.warning(f"[CacheAwareScheduler] 无效的调度模式 '{mode}',回退为 cache_affinity")
|
||||
logger.warning(
|
||||
f"[CacheAwareScheduler] 无效的调度模式 '{mode}',回退为 cache_affinity"
|
||||
)
|
||||
return self.SCHEDULING_MODE_CACHE_AFFINITY
|
||||
return normalized
|
||||
|
||||
@@ -1186,8 +1256,10 @@ class CacheAwareScheduler:
|
||||
logger.debug(f"[CacheAwareScheduler] 切换调度模式为: {self.scheduling_mode}")
|
||||
|
||||
def _apply_priority_mode_sort(
|
||||
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None,
|
||||
api_format: Optional[str] = None
|
||||
self,
|
||||
candidates: List[ProviderCandidate],
|
||||
affinity_key: Optional[str] = None,
|
||||
api_format: Optional[str] = None,
|
||||
) -> List[ProviderCandidate]:
|
||||
"""
|
||||
根据优先级模式对候选列表排序(数字越小越优先)
|
||||
@@ -1209,8 +1281,10 @@ class CacheAwareScheduler:
|
||||
return candidates
|
||||
|
||||
def _sort_by_global_priority_with_hash(
|
||||
self, candidates: List[ProviderCandidate], affinity_key: Optional[str] = None,
|
||||
api_format: Optional[str] = None
|
||||
self,
|
||||
candidates: List[ProviderCandidate],
|
||||
affinity_key: Optional[str] = None,
|
||||
api_format: Optional[str] = None,
|
||||
) -> List[ProviderCandidate]:
|
||||
"""
|
||||
按 global_priority_by_format 分组排序,同优先级内通过哈希分散实现负载均衡
|
||||
|
||||
@@ -30,7 +30,6 @@ from src.services.rate_limit.adaptive_rpm import get_adaptive_rpm_manager
|
||||
from src.services.rate_limit.detector import RateLimitType, detect_rate_limit_type
|
||||
|
||||
|
||||
|
||||
class ErrorAction(Enum):
|
||||
"""错误处理动作"""
|
||||
|
||||
@@ -391,10 +390,12 @@ class ErrorClassifier:
|
||||
current_usage=current_rpm,
|
||||
)
|
||||
|
||||
logger.info(f" [{request_id}] 429错误分析: "
|
||||
logger.info(
|
||||
f" [{request_id}] 429错误分析: "
|
||||
f"类型={rate_limit_info.limit_type}, "
|
||||
f"retry_after={rate_limit_info.retry_after}s, "
|
||||
f"当前RPM={current_rpm}")
|
||||
f"当前RPM={current_rpm}"
|
||||
)
|
||||
|
||||
# 调用自适应管理器处理
|
||||
new_limit = self.adaptive_manager.handle_429_error(
|
||||
@@ -408,7 +409,9 @@ class ErrorClassifier:
|
||||
logger.warning(f" [{request_id}] 并发限制触发(不调整RPM)")
|
||||
return "concurrent"
|
||||
elif rate_limit_info.limit_type == RateLimitType.RPM:
|
||||
logger.warning(f" [{request_id}] 自适应调整: Key {key.id[:8]}... RPM限制 -> {new_limit}")
|
||||
logger.warning(
|
||||
f" [{request_id}] 自适应调整: Key {key.id[:8]}... RPM限制 -> {new_limit}"
|
||||
)
|
||||
return "rpm"
|
||||
else:
|
||||
return "unknown"
|
||||
@@ -545,8 +548,10 @@ class ErrorClassifier:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.warning(f" [{request_id}] HTTP错误 (attempt={attempt}/{max_attempts}): "
|
||||
f"{http_error.response.status_code if http_error.response else 'unknown'}")
|
||||
logger.warning(
|
||||
f" [{request_id}] HTTP错误 (attempt={attempt}/{max_attempts}): "
|
||||
f"{http_error.response.status_code if http_error.response else 'unknown'}"
|
||||
)
|
||||
|
||||
converted_error = self.convert_http_error(http_error, provider_name, error_response_text)
|
||||
|
||||
@@ -557,16 +562,25 @@ class ErrorClassifier:
|
||||
if error_response_text:
|
||||
extra_data["error_response"] = error_response_text
|
||||
|
||||
# 转换 api_format 为字符串
|
||||
api_format_str = (
|
||||
# client_format:用于缓存亲和性/缓存失效(用户视角)
|
||||
client_format_str = (
|
||||
normalize_api_format(api_format).value
|
||||
if isinstance(api_format, (str, APIFormat))
|
||||
else str(api_format)
|
||||
)
|
||||
# provider_format:用于健康度/熔断 bucket(Provider 真实端点格式)
|
||||
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()
|
||||
|
||||
# 处理客户端请求错误(不应重试,不失效缓存,不记录健康失败)
|
||||
if isinstance(converted_error, UpstreamClientException):
|
||||
logger.warning(f" [{request_id}] 客户端请求错误,不进行重试: {converted_error.message}")
|
||||
logger.warning(
|
||||
f" [{request_id}] 客户端请求错误,不进行重试: {converted_error.message}"
|
||||
)
|
||||
return extra_data
|
||||
|
||||
# 处理认证错误
|
||||
@@ -574,7 +588,7 @@ class ErrorClassifier:
|
||||
if endpoint and key and self.cache_scheduler is not None:
|
||||
await self.cache_scheduler.invalidate_cache(
|
||||
affinity_key=affinity_key,
|
||||
api_format=api_format_str,
|
||||
api_format=client_format_str,
|
||||
global_model_id=global_model_id,
|
||||
endpoint_id=str(endpoint.id),
|
||||
key_id=str(key.id),
|
||||
@@ -583,7 +597,7 @@ class ErrorClassifier:
|
||||
health_monitor.record_failure(
|
||||
db=self.db,
|
||||
key_id=str(key.id),
|
||||
api_format=api_format_str,
|
||||
api_format=provider_format_str,
|
||||
error_type="ProviderAuthException",
|
||||
)
|
||||
return extra_data
|
||||
@@ -600,7 +614,7 @@ class ErrorClassifier:
|
||||
if endpoint and self.cache_scheduler is not None:
|
||||
await self.cache_scheduler.invalidate_cache(
|
||||
affinity_key=affinity_key,
|
||||
api_format=api_format_str,
|
||||
api_format=client_format_str,
|
||||
global_model_id=global_model_id,
|
||||
endpoint_id=str(endpoint.id),
|
||||
key_id=str(key.id),
|
||||
@@ -610,7 +624,7 @@ class ErrorClassifier:
|
||||
if endpoint and key and self.cache_scheduler is not None:
|
||||
await self.cache_scheduler.invalidate_cache(
|
||||
affinity_key=affinity_key,
|
||||
api_format=api_format_str,
|
||||
api_format=client_format_str,
|
||||
global_model_id=global_model_id,
|
||||
endpoint_id=str(endpoint.id),
|
||||
key_id=str(key.id),
|
||||
@@ -621,7 +635,7 @@ class ErrorClassifier:
|
||||
health_monitor.record_failure(
|
||||
db=self.db,
|
||||
key_id=str(key.id),
|
||||
api_format=api_format_str,
|
||||
api_format=provider_format_str,
|
||||
error_type=type(converted_error).__name__,
|
||||
)
|
||||
|
||||
@@ -662,15 +676,24 @@ class ErrorClassifier:
|
||||
"""
|
||||
provider_name = str(provider.name)
|
||||
|
||||
logger.warning(f" [{request_id}] 请求失败 (attempt={attempt}/{max_attempts}): "
|
||||
f"{type(error).__name__}: {str(error)}")
|
||||
logger.warning(
|
||||
f" [{request_id}] 请求失败 (attempt={attempt}/{max_attempts}): "
|
||||
f"{type(error).__name__}: {str(error)}"
|
||||
)
|
||||
|
||||
# 转换 api_format 为字符串
|
||||
api_format_str = (
|
||||
# client_format:用于缓存亲和性/缓存失效(用户视角)
|
||||
client_format_str = (
|
||||
normalize_api_format(api_format).value
|
||||
if isinstance(api_format, (str, APIFormat))
|
||||
else str(api_format)
|
||||
)
|
||||
# provider_format:用于健康度/熔断 bucket(Provider 真实端点格式)
|
||||
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()
|
||||
|
||||
# 处理限流错误
|
||||
if isinstance(error, ProviderRateLimitException) and key:
|
||||
@@ -684,7 +707,7 @@ class ErrorClassifier:
|
||||
if endpoint and self.cache_scheduler is not None:
|
||||
await self.cache_scheduler.invalidate_cache(
|
||||
affinity_key=affinity_key,
|
||||
api_format=api_format_str,
|
||||
api_format=client_format_str,
|
||||
global_model_id=global_model_id,
|
||||
endpoint_id=str(endpoint.id),
|
||||
key_id=str(key.id),
|
||||
@@ -693,7 +716,7 @@ class ErrorClassifier:
|
||||
# 其他错误也失效缓存
|
||||
await self.cache_scheduler.invalidate_cache(
|
||||
affinity_key=affinity_key,
|
||||
api_format=api_format_str,
|
||||
api_format=client_format_str,
|
||||
global_model_id=global_model_id,
|
||||
endpoint_id=str(endpoint.id),
|
||||
key_id=str(key.id),
|
||||
@@ -704,6 +727,6 @@ class ErrorClassifier:
|
||||
health_monitor.record_failure(
|
||||
db=self.db,
|
||||
key_id=str(key.id),
|
||||
api_format=api_format_str,
|
||||
api_format=provider_format_str,
|
||||
error_type=type(error).__name__,
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ import httpx
|
||||
from redis import Redis
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.api_format import APIFormat
|
||||
from src.core.api_format import APIFormat, FormatConversionError
|
||||
from src.core.error_utils import extract_error_message
|
||||
from src.core.exceptions import (
|
||||
ConcurrencyLimitError,
|
||||
@@ -385,7 +385,9 @@ 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": (
|
||||
api_format.value if hasattr(api_format, "value") else str(api_format)
|
||||
),
|
||||
}
|
||||
raise client_error
|
||||
else:
|
||||
@@ -425,10 +427,14 @@ class FallbackOrchestrator:
|
||||
# 检查是否为客户端请求错误(不应重试)
|
||||
converted_error = extra_data.get("converted_error")
|
||||
# 从 extra_data 中移除 converted_error,避免序列化问题
|
||||
serializable_extra_data = {k: v for k, v in extra_data.items() if k != "converted_error"}
|
||||
serializable_extra_data = {
|
||||
k: v for k, v in extra_data.items() if k != "converted_error"
|
||||
}
|
||||
|
||||
if isinstance(converted_error, UpstreamClientException):
|
||||
logger.warning(f" [{request_id}] 客户端请求错误,停止重试: {converted_error.message}")
|
||||
logger.warning(
|
||||
f" [{request_id}] 客户端请求错误,停止重试: {converted_error.message}"
|
||||
)
|
||||
RequestCandidateService.mark_candidate_failed(
|
||||
db=self.db,
|
||||
candidate_id=candidate_record_id,
|
||||
@@ -445,7 +451,9 @@ 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": (
|
||||
api_format.value if hasattr(api_format, "value") else str(api_format)
|
||||
),
|
||||
}
|
||||
raise converted_error
|
||||
|
||||
@@ -487,6 +495,19 @@ class FallbackOrchestrator:
|
||||
)
|
||||
return "continue" if has_retry_left else "break"
|
||||
|
||||
# 格式转换错误:视为候选不可用,直接切换到下一个候选(不记录健康失败)
|
||||
if isinstance(cause, FormatConversionError):
|
||||
logger.warning(f" [{request_id}] 格式转换失败,切换候选: {cause}")
|
||||
RequestCandidateService.mark_candidate_failed(
|
||||
db=self.db,
|
||||
candidate_id=candidate_record_id,
|
||||
error_type="FormatConversionError",
|
||||
error_message=str(cause),
|
||||
latency_ms=elapsed_ms,
|
||||
concurrent_requests=captured_key_concurrent,
|
||||
)
|
||||
return "break"
|
||||
|
||||
# 未知错误:记录失败并抛出
|
||||
RequestCandidateService.mark_candidate_failed(
|
||||
db=self.db,
|
||||
@@ -552,8 +573,10 @@ class FallbackOrchestrator:
|
||||
last_candidate = candidate
|
||||
|
||||
if candidate.is_skipped:
|
||||
logger.debug(f" [{request_id}] 跳过候选: Provider={candidate.provider.name}, "
|
||||
f"Reason={candidate.skip_reason}")
|
||||
logger.debug(
|
||||
f" [{request_id}] 跳过候选: Provider={candidate.provider.name}, "
|
||||
f"Reason={candidate.skip_reason}"
|
||||
)
|
||||
continue
|
||||
|
||||
result = await self._try_candidate_with_retries(
|
||||
@@ -573,7 +596,9 @@ class FallbackOrchestrator:
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
response: Tuple[Any, str, Optional[str], Optional[str], Optional[str], Optional[str]] = result["response"]
|
||||
response: Tuple[
|
||||
Any, str, Optional[str], Optional[str], Optional[str], Optional[str]
|
||||
] = result["response"]
|
||||
return response
|
||||
|
||||
# 更新计数器和错误信息
|
||||
@@ -582,7 +607,9 @@ 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_enum
|
||||
)
|
||||
raise last_error
|
||||
|
||||
# 所有组合都已尝试完毕,全部失败
|
||||
@@ -620,9 +647,13 @@ class FallbackOrchestrator:
|
||||
if retry_index == 0:
|
||||
# 首次尝试该候选
|
||||
cache_hint = " (cached)" if candidate.is_cached else ""
|
||||
logger.info(f" [{request_id[:8] if request_id else 'N/A'}] -> {provider.name}{cache_hint}")
|
||||
logger.info(
|
||||
f" [{request_id[:8] if request_id else 'N/A'}] -> {provider.name}{cache_hint}"
|
||||
)
|
||||
else:
|
||||
logger.info(f" [{request_id[:8] if request_id else 'N/A'}] -> {provider.name} (retry {retry_index})")
|
||||
logger.info(
|
||||
f" [{request_id[:8] if request_id else 'N/A'}] -> {provider.name} (retry {retry_index})"
|
||||
)
|
||||
|
||||
candidate_record_id = candidate_record_map[(candidate_index, retry_index)]
|
||||
|
||||
@@ -706,14 +737,14 @@ class FallbackOrchestrator:
|
||||
),
|
||||
provider=getattr(existing_metadata, "provider", None) or str(candidate.provider.name),
|
||||
model=getattr(existing_metadata, "model", None) or model_name,
|
||||
provider_id=getattr(existing_metadata, "provider_id", None) or str(candidate.provider.id),
|
||||
provider_id=getattr(existing_metadata, "provider_id", None)
|
||||
or str(candidate.provider.id),
|
||||
provider_endpoint_id=(
|
||||
getattr(existing_metadata, "provider_endpoint_id", None)
|
||||
or str(candidate.endpoint.id)
|
||||
),
|
||||
provider_api_key_id=(
|
||||
getattr(existing_metadata, "provider_api_key_id", None)
|
||||
or str(candidate.key.id)
|
||||
getattr(existing_metadata, "provider_api_key_id", None) or str(candidate.key.id)
|
||||
),
|
||||
api_format=api_format_enum.value,
|
||||
)
|
||||
@@ -821,12 +852,16 @@ class FallbackOrchestrator:
|
||||
user_id = str(user_api_key.user_id)
|
||||
api_format_enum = normalize_api_format(api_format)
|
||||
|
||||
logger.debug(f"[FallbackOrchestrator] execute_with_fallback 被调用: "
|
||||
logger.debug(
|
||||
f"[FallbackOrchestrator] execute_with_fallback 被调用: "
|
||||
f"api_format={api_format_enum.value}, model_name={model_name}, "
|
||||
f"request_id={request_id}, is_stream={is_stream}")
|
||||
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)
|
||||
self._create_pending_usage_record(
|
||||
request_id, user_api_key, model_name, is_stream, api_format_enum
|
||||
)
|
||||
|
||||
# 1. 收集所有候选(同时获取规范化的 global_model_id 用于缓存亲和性)
|
||||
all_candidates, global_model_id = await self._fetch_all_candidates(
|
||||
|
||||
@@ -12,11 +12,11 @@ from src.core.api_format import APIFormat
|
||||
from src.core.exceptions import ConcurrencyLimitError
|
||||
from src.core.logger import logger
|
||||
from src.services.health.monitor import health_monitor
|
||||
from src.services.provider.format import normalize_api_format
|
||||
from src.services.rate_limit.adaptive_reservation import get_adaptive_reservation_manager
|
||||
from src.services.request.candidate import RequestCandidateService
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionContext:
|
||||
candidate_id: str
|
||||
@@ -103,7 +103,9 @@ class RequestExecutor:
|
||||
# 获取有效的 RPM 限制(自适应或固定)
|
||||
if key.rpm_limit is None:
|
||||
# 自适应模式:使用学习值,未学习时为 None(不限制,等待碰壁学习)
|
||||
effective_key_limit = int(key.learned_rpm_limit) if key.learned_rpm_limit is not None else None
|
||||
effective_key_limit = (
|
||||
int(key.learned_rpm_limit) if key.learned_rpm_limit is not None else None
|
||||
)
|
||||
else:
|
||||
effective_key_limit = int(key.rpm_limit)
|
||||
|
||||
@@ -114,9 +116,11 @@ class RequestExecutor:
|
||||
)
|
||||
dynamic_reservation_ratio = reservation_result.ratio
|
||||
|
||||
logger.debug(f"[Executor] 动态预留: key={key.id[:8]}..., "
|
||||
logger.debug(
|
||||
f"[Executor] 动态预留: key={key.id[:8]}..., "
|
||||
f"ratio={dynamic_reservation_ratio:.0%}, phase={reservation_result.phase}, "
|
||||
f"confidence={reservation_result.confidence:.0%}")
|
||||
f"confidence={reservation_result.confidence:.0%}"
|
||||
)
|
||||
|
||||
async with self.concurrency_manager.rpm_guard(
|
||||
key_id=key.id,
|
||||
@@ -140,12 +144,21 @@ class RequestExecutor:
|
||||
|
||||
context.elapsed_ms = int((time.time() - context.start_time) * 1000)
|
||||
|
||||
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 = (
|
||||
api_format.value if isinstance(api_format, APIFormat) else str(api_format)
|
||||
)
|
||||
health_format = normalize_api_format(provider_format_str or client_format_str).value
|
||||
|
||||
health_monitor.record_success(
|
||||
db=self.db,
|
||||
key_id=key.id,
|
||||
api_format=(
|
||||
api_format.value if isinstance(api_format, APIFormat) else api_format
|
||||
),
|
||||
api_format=health_format,
|
||||
response_time_ms=context.elapsed_ms,
|
||||
)
|
||||
|
||||
@@ -180,7 +193,9 @@ class RequestExecutor:
|
||||
"is_cached_user": is_cached_user,
|
||||
"model_name": model_name,
|
||||
"api_format": (
|
||||
api_format.value if isinstance(api_format, APIFormat) else api_format
|
||||
api_format.value
|
||||
if isinstance(api_format, APIFormat)
|
||||
else api_format
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -118,6 +118,10 @@ class SystemConfigService:
|
||||
"value": "cache_affinity",
|
||||
"description": "调度模式:fixed_order(固定顺序模式,严格按优先级顺序) 或 cache_affinity(缓存亲和模式,优先使用已缓存的Provider)",
|
||||
},
|
||||
"format_conversion_enabled": {
|
||||
"value": False,
|
||||
"description": "是否启用全局格式自动转换(需要端点配置 format_acceptance_config 才能生效)",
|
||||
},
|
||||
"auto_delete_expired_keys": {
|
||||
"value": False,
|
||||
"description": "是否自动删除过期的API Key(True=物理删除,False=仅禁用),仅管理员可配置",
|
||||
|
||||
Reference in New Issue
Block a user