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:
fawney19
2026-02-16 11:00:48 +08:00
parent 4dc401677d
commit 63870931af
85 changed files with 1907 additions and 1463 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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