2026-02-02 03:16:52 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
2026-02-02 21:16:28 +08:00
|
|
|
from src.models.database import ApiKey
|
2026-02-02 03:16:52 +08:00
|
|
|
from src.services.orchestration.error_classifier import ErrorClassifier
|
2026-02-16 11:00:48 +08:00
|
|
|
from src.services.scheduling.aware_scheduler import ProviderCandidate, get_cache_aware_scheduler
|
2026-02-02 03:16:52 +08:00
|
|
|
from src.services.system.config import SystemConfigService
|
|
|
|
|
|
|
|
|
|
from .recorder import CandidateRecorder
|
|
|
|
|
from .resolver import CandidateResolver
|
|
|
|
|
from .schema import CandidateKey
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CandidateService:
|
|
|
|
|
"""
|
|
|
|
|
CandidateService (Facade).
|
|
|
|
|
|
|
|
|
|
Phase2 note: this is introduced as a new domain entrypoint. Legacy orchestrators
|
|
|
|
|
still exist and will be migrated gradually to use this service.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, db: Session, redis_client: Any | None = None) -> None:
|
|
|
|
|
self.db = db
|
|
|
|
|
self.redis = redis_client
|
|
|
|
|
self._cache_scheduler = None
|
|
|
|
|
self._resolver: CandidateResolver | None = None
|
|
|
|
|
self._error_classifier: ErrorClassifier | None = None
|
|
|
|
|
self._recorder = CandidateRecorder(db)
|
|
|
|
|
|
|
|
|
|
async def _ensure_initialized(self) -> None:
|
|
|
|
|
if self._cache_scheduler is not None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
priority_mode = SystemConfigService.get_config(
|
|
|
|
|
self.db,
|
|
|
|
|
"provider_priority_mode",
|
|
|
|
|
"provider",
|
|
|
|
|
)
|
|
|
|
|
scheduling_mode = SystemConfigService.get_config(
|
|
|
|
|
self.db,
|
|
|
|
|
"scheduling_mode",
|
|
|
|
|
"cache_affinity",
|
|
|
|
|
)
|
|
|
|
|
self._cache_scheduler = await get_cache_aware_scheduler(
|
|
|
|
|
self.redis,
|
|
|
|
|
priority_mode=priority_mode,
|
|
|
|
|
scheduling_mode=scheduling_mode,
|
|
|
|
|
)
|
|
|
|
|
self._resolver = CandidateResolver(db=self.db, cache_scheduler=self._cache_scheduler)
|
|
|
|
|
self._error_classifier = ErrorClassifier(db=self.db, cache_scheduler=self._cache_scheduler)
|
|
|
|
|
|
|
|
|
|
async def resolve(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
api_format: str,
|
|
|
|
|
model_name: str,
|
|
|
|
|
affinity_key: str,
|
|
|
|
|
user_api_key: ApiKey | None = None,
|
|
|
|
|
request_id: str | None = None,
|
|
|
|
|
is_stream: bool = False,
|
|
|
|
|
capability_requirements: dict[str, bool] | None = None,
|
|
|
|
|
preferred_key_ids: list[str] | None = None,
|
|
|
|
|
) -> tuple[list[ProviderCandidate], str]:
|
|
|
|
|
await self._ensure_initialized()
|
|
|
|
|
assert self._resolver is not None
|
|
|
|
|
return await self._resolver.fetch_candidates(
|
|
|
|
|
api_format=api_format,
|
|
|
|
|
model_name=model_name,
|
|
|
|
|
affinity_key=affinity_key,
|
|
|
|
|
user_api_key=user_api_key,
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
is_stream=is_stream,
|
|
|
|
|
capability_requirements=capability_requirements,
|
|
|
|
|
preferred_key_ids=preferred_key_ids,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def create_candidate_records(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
candidates: list[ProviderCandidate],
|
|
|
|
|
request_id: str,
|
|
|
|
|
user_api_key: ApiKey,
|
|
|
|
|
required_capabilities: dict[str, bool] | None = None,
|
|
|
|
|
expand_retries: bool = True,
|
|
|
|
|
) -> dict[tuple[int, int], str]:
|
|
|
|
|
# CandidateResolver.create_candidate_records is currently the canonical implementation
|
|
|
|
|
assert self._resolver is not None, "Call resolve() once before create_candidate_records()"
|
|
|
|
|
return self._resolver.create_candidate_records(
|
|
|
|
|
all_candidates=candidates,
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
user_id=str(user_api_key.user_id),
|
|
|
|
|
user_api_key=user_api_key,
|
|
|
|
|
required_capabilities=required_capabilities,
|
|
|
|
|
expand_retries=expand_retries,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_candidate_keys(self, request_id: str) -> list["CandidateKey"]:
|
|
|
|
|
return self._recorder.get_candidate_keys(request_id)
|