mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 调度器迁移至独立模块,消除 services->api 反向依赖
- 将调度器相关模块从 src/services/cache/ 迁移到 src/services/scheduling/ - 下沉类型定义到 core 层: AccessRestrictions, ProviderAuthInfo, ParsedChunk/StreamStats, 视频工具函数 - 提取 thinking_cache 签名缓存到 core/api_format/conversion/ - 提取 provider 认证逻辑到 services/provider/auth - 提取遥测记录到 services/usage/telemetry - 提取 models 列表缓存到 services/cache/model_list_cache - 更新所有引用方的 import 路径及相关测试
This commit is contained in:
@@ -2,10 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.api.handlers.base.cli_protocol import CliHandlerProtocol
|
||||
from src.api.handlers.base.stream_context import StreamContext
|
||||
from src.api.handlers.gemini_cli.handler import GeminiCliMessageHandler
|
||||
from src.services.provider.adapters.antigravity.envelope import (
|
||||
@@ -110,7 +112,9 @@ def test_handle_sse_event_unwraps_for_antigravity() -> None:
|
||||
}
|
||||
|
||||
with patch.object(handler, "_process_event_data") as mock_process:
|
||||
handler._handle_sse_event(ctx, None, json.dumps(v1_data), record_chunk=False)
|
||||
cast(CliHandlerProtocol, handler)._handle_sse_event(
|
||||
ctx, None, json.dumps(v1_data), record_chunk=False
|
||||
)
|
||||
|
||||
assert mock_process.call_count == 1
|
||||
passed_data = mock_process.call_args[0][2]
|
||||
@@ -120,7 +124,7 @@ def test_handle_sse_event_unwraps_for_antigravity() -> None:
|
||||
|
||||
|
||||
def test_handle_sse_event_caches_thought_signature_for_antigravity() -> None:
|
||||
from src.services.provider.adapters.antigravity.signature_cache import signature_cache
|
||||
from src.core.api_format.conversion.thinking_cache import signature_cache
|
||||
|
||||
signature_cache.clear()
|
||||
|
||||
@@ -148,7 +152,9 @@ def test_handle_sse_event_caches_thought_signature_for_antigravity() -> None:
|
||||
}
|
||||
|
||||
with patch.object(handler, "_process_event_data") as _mock_process:
|
||||
handler._handle_sse_event(ctx, None, json.dumps(payload), record_chunk=False)
|
||||
cast(CliHandlerProtocol, handler)._handle_sse_event(
|
||||
ctx, None, json.dumps(payload), record_chunk=False
|
||||
)
|
||||
|
||||
assert signature_cache.get_or_dummy("claude-sonnet-4-5", "t1") == long_sig
|
||||
|
||||
@@ -206,7 +212,7 @@ async def test_antigravity_forces_conversion_path_in_stream_with_prefetch() -> N
|
||||
|
||||
|
||||
def test_wrap_v1internal_request_injects_thought_signature_from_tool_cache() -> None:
|
||||
from src.services.provider.adapters.antigravity.signature_cache import signature_cache
|
||||
from src.core.api_format.conversion.thinking_cache import signature_cache
|
||||
|
||||
signature_cache.clear()
|
||||
|
||||
@@ -234,7 +240,7 @@ def test_wrap_v1internal_request_injects_thought_signature_from_tool_cache() ->
|
||||
|
||||
|
||||
def test_wrap_v1internal_request_injects_session_signature_when_tool_cache_missing() -> None:
|
||||
from src.services.provider.adapters.antigravity.signature_cache import signature_cache
|
||||
from src.core.api_format.conversion.thinking_cache import signature_cache
|
||||
|
||||
signature_cache.clear()
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.cache.aware_scheduler import CacheAwareScheduler, ProviderCandidate
|
||||
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
||||
from src.services.scheduling.aware_scheduler import CacheAwareScheduler, ProviderCandidate
|
||||
|
||||
|
||||
def _make_candidate(
|
||||
@@ -31,15 +33,15 @@ def _make_candidate(
|
||||
)
|
||||
|
||||
return ProviderCandidate(
|
||||
provider=provider,
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
provider=cast(Provider, provider),
|
||||
endpoint=cast(ProviderEndpoint, endpoint),
|
||||
key=cast(ProviderAPIKey, key),
|
||||
is_cached=False,
|
||||
is_skipped=is_skipped,
|
||||
skip_reason="unhealthy" if is_skipped else None,
|
||||
needs_conversion=needs_conversion,
|
||||
provider_api_format="openai:chat",
|
||||
) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.cache.aware_scheduler import CacheAwareScheduler
|
||||
from src.services.scheduling.aware_scheduler import CacheAwareScheduler
|
||||
|
||||
|
||||
def _make_db() -> MagicMock:
|
||||
@@ -71,11 +71,11 @@ async def test_list_all_candidates_returns_provider_batch_count_even_when_candid
|
||||
with patch.object(scheduler, "_ensure_initialized", new=AsyncMock(return_value=None)):
|
||||
with patch.object(scheduler._candidate_builder, "_query_providers", return_value=providers):
|
||||
with patch(
|
||||
"src.services.cache.aware_scheduler.ModelCacheService.get_global_model_by_name",
|
||||
"src.services.scheduling.aware_scheduler.ModelCacheService.get_global_model_by_name",
|
||||
new=AsyncMock(return_value=global_model),
|
||||
):
|
||||
with patch(
|
||||
"src.services.cache.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
|
||||
"src.services.scheduling.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
candidates, global_model_id, provider_batch_count = (
|
||||
@@ -108,11 +108,11 @@ async def test_list_all_candidates_returns_zero_provider_batch_count_when_provid
|
||||
with patch.object(scheduler, "_ensure_initialized", new=AsyncMock(return_value=None)):
|
||||
with patch.object(scheduler._candidate_builder, "_query_providers", return_value=[]):
|
||||
with patch(
|
||||
"src.services.cache.aware_scheduler.ModelCacheService.get_global_model_by_name",
|
||||
"src.services.scheduling.aware_scheduler.ModelCacheService.get_global_model_by_name",
|
||||
new=AsyncMock(return_value=global_model),
|
||||
):
|
||||
with patch(
|
||||
"src.services.cache.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
|
||||
"src.services.scheduling.aware_scheduler.SystemConfigService.is_format_conversion_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
candidates, global_model_id, provider_batch_count = (
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src.core.api_format.conversion.thinking_cache import ThinkingSignatureCache
|
||||
from src.services.provider.adapters.antigravity.constants import DUMMY_THOUGHT_SIGNATURE
|
||||
from src.services.provider.adapters.antigravity.signature_cache import ThinkingSignatureCache
|
||||
|
||||
# 测试用签名(需 >= MIN_SIGNATURE_LENGTH=50)
|
||||
_SIG_A = "a" * 60
|
||||
@@ -54,11 +54,11 @@ def test_tool_signature_short_ignored() -> None:
|
||||
|
||||
|
||||
def test_tool_signature_cache_enforces_limit(monkeypatch: Any) -> None:
|
||||
import src.services.provider.adapters.antigravity.signature_cache as sc_mod
|
||||
import src.core.api_format.conversion.thinking_cache as tc_mod
|
||||
|
||||
# Use a small limit to make eviction deterministic in tests.
|
||||
monkeypatch.setattr(sc_mod, "_TOOL_CACHE_LIMIT", 3)
|
||||
cache = sc_mod.ThinkingSignatureCache()
|
||||
monkeypatch.setattr(tc_mod, "_TOOL_CACHE_LIMIT", 3)
|
||||
cache = tc_mod.ThinkingSignatureCache()
|
||||
cache.cache_tool_signature("toolu_1", _SIG_A)
|
||||
cache.cache_tool_signature("toolu_2", _SIG_B)
|
||||
cache.cache_tool_signature("toolu_3", _SIG_C)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from src.api.handlers.base.utils import get_format_converter_registry
|
||||
from src.core.api_format.conversion.thinking_cache import signature_cache
|
||||
from src.services.provider.adapters.antigravity.constants import DUMMY_THOUGHT_SIGNATURE
|
||||
from src.services.provider.adapters.antigravity.signature_cache import signature_cache
|
||||
|
||||
|
||||
def _reset_sig_cache() -> None:
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from src.models.database import GlobalModel, Model, Provider
|
||||
from src.services.cache.aware_scheduler import CacheAwareScheduler
|
||||
from src.services.scheduling.aware_scheduler import CacheAwareScheduler
|
||||
|
||||
|
||||
class TestCheckModelSupportForGlobalModel:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.services.cache.aware_scheduler import CacheAwareScheduler
|
||||
from src.services.scheduling.aware_scheduler import CacheAwareScheduler
|
||||
|
||||
|
||||
def _make_key(
|
||||
@@ -20,7 +20,7 @@ def _make_key(
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_kiro_quota_remaining_zero_skips(_mock_cb: MagicMock) -> None:
|
||||
@@ -39,7 +39,7 @@ def test_kiro_quota_remaining_zero_skips(_mock_cb: MagicMock) -> None:
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_kiro_quota_remaining_positive_allows(_mock_cb: MagicMock) -> None:
|
||||
@@ -58,7 +58,7 @@ def test_kiro_quota_remaining_positive_allows(_mock_cb: MagicMock) -> None:
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_codex_weekly_quota_exhausted_skips(_mock_cb: MagicMock) -> None:
|
||||
@@ -85,7 +85,7 @@ def test_codex_weekly_quota_exhausted_skips(_mock_cb: MagicMock) -> None:
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_codex_5h_quota_exhausted_skips(_mock_cb: MagicMock) -> None:
|
||||
@@ -111,7 +111,7 @@ def test_codex_5h_quota_exhausted_skips(_mock_cb: MagicMock) -> None:
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_codex_ignores_code_review_quota(_mock_cb: MagicMock) -> None:
|
||||
@@ -138,7 +138,7 @@ def test_codex_ignores_code_review_quota(_mock_cb: MagicMock) -> None:
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_antigravity_model_quota_exhausted_skips(_mock_cb: MagicMock) -> None:
|
||||
@@ -166,7 +166,7 @@ def test_antigravity_model_quota_exhausted_skips(_mock_cb: MagicMock) -> None:
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_antigravity_other_model_not_exhausted_allows(_mock_cb: MagicMock) -> None:
|
||||
@@ -194,7 +194,7 @@ def test_antigravity_other_model_not_exhausted_allows(_mock_cb: MagicMock) -> No
|
||||
|
||||
|
||||
@patch(
|
||||
"src.services.cache.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
"src.services.scheduling.candidate_builder.health_monitor.get_circuit_breaker_status",
|
||||
return_value=(True, None),
|
||||
)
|
||||
def test_antigravity_quota_uses_mapping_matched_model(_mock_cb: MagicMock) -> None:
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.cache.aware_scheduler import CacheAwareScheduler, ProviderCandidate
|
||||
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
||||
from src.services.orchestration.candidate_resolver import CandidateResolver
|
||||
from src.services.scheduling.aware_scheduler import CacheAwareScheduler, ProviderCandidate
|
||||
|
||||
|
||||
class _FakeScheduler:
|
||||
@@ -89,19 +90,19 @@ def _make_global_key_candidate(*, key_id: str, priority: int) -> ProviderCandida
|
||||
global_priority_by_format={"openai:chat": priority},
|
||||
)
|
||||
return ProviderCandidate(
|
||||
provider=provider,
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
provider=cast(Provider, provider),
|
||||
endpoint=cast(ProviderEndpoint, endpoint),
|
||||
key=cast(ProviderAPIKey, key),
|
||||
needs_conversion=False,
|
||||
provider_api_format="openai:chat",
|
||||
) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_candidate_resolver_pagination_continues_on_empty_candidate_batch() -> None:
|
||||
db = MagicMock()
|
||||
scheduler = _FakeScheduler()
|
||||
resolver = CandidateResolver(db=db, cache_scheduler=scheduler) # type: ignore[arg-type]
|
||||
resolver = CandidateResolver(db=db, cache_scheduler=cast(CacheAwareScheduler, scheduler))
|
||||
|
||||
candidates, global_model_id = await resolver.fetch_candidates(
|
||||
api_format="openai:chat",
|
||||
|
||||
@@ -50,7 +50,7 @@ async def test_submit_with_failover_skips_http_500_then_succeeds(
|
||||
lambda *_args, **_kwargs: "provider",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.cache.aware_scheduler.get_cache_aware_scheduler",
|
||||
"src.services.scheduling.aware_scheduler.get_cache_aware_scheduler",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -106,7 +106,7 @@ async def test_submit_with_failover_stops_on_client_error(monkeypatch: pytest.Mo
|
||||
lambda *_args, **_kwargs: "provider",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.cache.aware_scheduler.get_cache_aware_scheduler",
|
||||
"src.services.scheduling.aware_scheduler.get_cache_aware_scheduler",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -152,7 +152,7 @@ async def test_submit_with_failover_no_eligible_candidates_due_to_auth_type(
|
||||
lambda *_args, **_kwargs: "provider",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.cache.aware_scheduler.get_cache_aware_scheduler",
|
||||
"src.services.scheduling.aware_scheduler.get_cache_aware_scheduler",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -194,7 +194,7 @@ async def test_submit_with_failover_filters_missing_billing_rule(
|
||||
lambda *_args, **_kwargs: "provider",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.cache.aware_scheduler.get_cache_aware_scheduler",
|
||||
"src.services.scheduling.aware_scheduler.get_cache_aware_scheduler",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
import pytest
|
||||
|
||||
from src.core.api_format.conversion import register_default_normalizers
|
||||
from src.services.cache.aware_scheduler import (
|
||||
from src.services.scheduling.aware_scheduler import (
|
||||
CacheAwareScheduler,
|
||||
_sort_endpoints_by_family_priority,
|
||||
)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.services.cache.candidate_sorter import CandidateSorter
|
||||
from src.services.cache.scheduling_config import SchedulingConfig
|
||||
from src.services.cache.schemas import ProviderCandidate
|
||||
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
||||
from src.services.scheduling.candidate_sorter import CandidateSorter
|
||||
from src.services.scheduling.scheduling_config import SchedulingConfig
|
||||
from src.services.scheduling.schemas import ProviderCandidate
|
||||
|
||||
|
||||
def _make_candidate(
|
||||
@@ -28,12 +30,12 @@ def _make_candidate(
|
||||
global_priority_by_format={"openai:chat": global_priority},
|
||||
)
|
||||
return ProviderCandidate(
|
||||
provider=provider,
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
provider=cast(Provider, provider),
|
||||
endpoint=cast(ProviderEndpoint, endpoint),
|
||||
key=cast(ProviderAPIKey, key),
|
||||
needs_conversion=needs_conversion,
|
||||
provider_api_format="openai:chat",
|
||||
) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def test_priority_sort_global_key_does_not_demote_when_global_keep_priority_enabled() -> None:
|
||||
@@ -59,7 +61,7 @@ def test_priority_sort_global_key_does_not_demote_when_global_keep_priority_enab
|
||||
|
||||
# 全局 keep_priority_on_conversion=True:不做 needs_conversion 降级分组,纯按 global_priority 排序
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
"src.services.scheduling.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=True,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([exact, demoted], db, None, "openai:chat")
|
||||
@@ -90,7 +92,7 @@ def test_priority_sort_global_key_demotes_convertible_when_global_keep_priority_
|
||||
|
||||
# 全局 keep_priority_on_conversion=False:需要降级的 convertible 候选整体排后
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
"src.services.scheduling.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=False,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([exact, demoted], db, None, "openai:chat")
|
||||
@@ -126,7 +128,7 @@ def test_priority_sort_global_key_provider_keep_priority_overrides_demotion_grou
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
"src.services.scheduling.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=False,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort(
|
||||
@@ -163,7 +165,7 @@ def test_priority_sort_provider_mode_demotes_convertible_when_global_keep_priori
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
"src.services.scheduling.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=False,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([demoted, exact], db, None, "openai:chat")
|
||||
@@ -193,7 +195,7 @@ def test_priority_sort_provider_mode_does_not_demote_when_global_keep_priority_e
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
"src.services.scheduling.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=True,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([demoted, exact], db, None, "openai:chat")
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.services.cache.aware_scheduler import ProviderCandidate
|
||||
from src.services.scheduling.aware_scheduler import ProviderCandidate
|
||||
|
||||
|
||||
def _make_candidate(
|
||||
|
||||
Reference in New Issue
Block a user