mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 统一任务框架 Phase 3 - 用 TaskService/FailoverEngine 替代 FallbackOrchestrator
核心重构:
- 移除 FallbackOrchestrator,用 TaskService + FailoverEngine 替代
- TaskService 作为统一入口,支持 SYNC/ASYNC 两种任务模式
- FailoverEngine 实现候选遍历、重试、故障转移逻辑
- 新增 AttemptFunc/AttemptResult 协议,统一尝试结果表示
功能改进:
- 流式响应首字节探测(30s 超时,空流触发故障转移)
- 流式取消归因优化(区分客户端断连 vs 服务端中断)
- 新增 OpenAI Sora 视频取消路由 POST /v1/videos/{task_id}/cancel
- OpenAI 流式请求自动添加 stream_options.include_usage
代码规范:
- 修复 loguru 日志格式(%s → {})
- 新增 FORMAT_CONVERSION_ENABLED 环境变量说明
测试覆盖:
- test_failover_engine.py: FailoverEngine 单元测试
- test_task_service_async_execute.py: TaskService ASYNC 模式测试
- test_video_cancel_e2e.py: 视频取消端到端测试
This commit is contained in:
@@ -28,7 +28,9 @@ def _mock_endpoint(api_format: str, config: dict | None = None) -> MagicMock:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_candidates_blocks_cross_format_when_global_switch_off() -> None:
|
||||
async def test_build_candidates_allows_cross_format_when_endpoint_accepts_and_overrides_off() -> (
|
||||
None
|
||||
):
|
||||
register_default_normalizers()
|
||||
|
||||
scheduler = CacheAwareScheduler()
|
||||
@@ -37,6 +39,41 @@ async def test_build_candidates_blocks_cross_format_when_global_switch_off() ->
|
||||
|
||||
provider = MagicMock()
|
||||
provider.name = "p1"
|
||||
provider.enable_format_conversion = False
|
||||
provider.endpoints = [
|
||||
_mock_endpoint(
|
||||
"openai:chat",
|
||||
{"enabled": True, "accept_formats": ["claude:chat"], "stream_conversion": True},
|
||||
)
|
||||
]
|
||||
provider.api_keys = [_mock_key("k1", ["openai:chat"])]
|
||||
|
||||
candidates = await scheduler._build_candidates(
|
||||
db=MagicMock(),
|
||||
providers=[provider],
|
||||
client_format="claude:chat",
|
||||
model_name="dummy-model",
|
||||
affinity_key=None,
|
||||
global_conversion_enabled=False, # DB 全局覆盖关闭
|
||||
master_conversion_enabled=True, # ENV 总闸开启(默认)
|
||||
)
|
||||
|
||||
assert len(candidates) == 1
|
||||
assert candidates[0].needs_conversion is True
|
||||
assert candidates[0].provider_api_format == "openai:chat"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_candidates_blocks_cross_format_when_master_switch_off() -> None:
|
||||
register_default_normalizers()
|
||||
|
||||
scheduler = CacheAwareScheduler()
|
||||
scheduler._check_model_support = AsyncMock(return_value=(True, None, None, {"m"})) # type: ignore[attr-defined]
|
||||
scheduler._check_key_availability = MagicMock(return_value=(True, None, None)) # type: ignore[attr-defined]
|
||||
|
||||
provider = MagicMock()
|
||||
provider.name = "p1"
|
||||
provider.enable_format_conversion = False
|
||||
provider.endpoints = [
|
||||
_mock_endpoint(
|
||||
"openai:chat",
|
||||
@@ -52,6 +89,7 @@ async def test_build_candidates_blocks_cross_format_when_global_switch_off() ->
|
||||
model_name="dummy-model",
|
||||
affinity_key=None,
|
||||
global_conversion_enabled=False,
|
||||
master_conversion_enabled=False,
|
||||
)
|
||||
|
||||
assert candidates == []
|
||||
@@ -67,11 +105,10 @@ async def test_build_candidates_includes_cross_format_when_enabled() -> None:
|
||||
|
||||
provider = MagicMock()
|
||||
provider.name = "p1"
|
||||
provider.enable_format_conversion = False
|
||||
provider.endpoints = [
|
||||
_mock_endpoint(
|
||||
"openai:chat",
|
||||
{"enabled": True, "accept_formats": ["claude:chat"], "stream_conversion": True},
|
||||
)
|
||||
# 端点未配置/未启用格式接受策略,但 DB 全局覆盖开启应强制允许
|
||||
_mock_endpoint("openai:chat", None)
|
||||
]
|
||||
provider.api_keys = [_mock_key("k1", ["openai:chat"])]
|
||||
|
||||
@@ -81,7 +118,8 @@ async def test_build_candidates_includes_cross_format_when_enabled() -> None:
|
||||
client_format="claude:chat",
|
||||
model_name="dummy-model",
|
||||
affinity_key=None,
|
||||
global_conversion_enabled=True,
|
||||
global_conversion_enabled=True, # DB 全局覆盖开启:跳过端点检查
|
||||
master_conversion_enabled=True,
|
||||
)
|
||||
|
||||
assert len(candidates) == 1
|
||||
@@ -99,6 +137,7 @@ async def test_exact_matches_rank_before_convertible() -> None:
|
||||
|
||||
provider = MagicMock()
|
||||
provider.name = "p1"
|
||||
provider.enable_format_conversion = False
|
||||
# 故意把 OPENAI 放在 endpoints[0],验证排序仍然是 CLAUDE(exact)在前
|
||||
provider.endpoints = [
|
||||
_mock_endpoint(
|
||||
@@ -118,7 +157,8 @@ async def test_exact_matches_rank_before_convertible() -> None:
|
||||
client_format="claude:chat",
|
||||
model_name="dummy-model",
|
||||
affinity_key=None,
|
||||
global_conversion_enabled=True,
|
||||
global_conversion_enabled=False,
|
||||
master_conversion_enabled=True,
|
||||
)
|
||||
|
||||
assert len(candidates) == 2
|
||||
|
||||
Reference in New Issue
Block a user