2026-02-15 16:32:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
调度器核心数据类型
|
|
|
|
|
|
|
|
|
|
|
|
从 CacheAwareScheduler 提取的共享数据结构,被 24+ 个模块使用。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-03-03 17:24:22 +08:00
|
|
|
|
from dataclasses import dataclass, field
|
2026-03-09 13:26:54 +08:00
|
|
|
|
from typing import TYPE_CHECKING, Any
|
2026-02-15 16:32:23 +08:00
|
|
|
|
|
|
|
|
|
|
from src.models.database import (
|
|
|
|
|
|
Provider,
|
|
|
|
|
|
ProviderAPIKey,
|
|
|
|
|
|
ProviderEndpoint,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-03 17:24:22 +08:00
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from src.services.provider.pool.config import PoolConfig
|
|
|
|
|
|
|
2026-02-15 16:32:23 +08:00
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class ProviderCandidate:
|
|
|
|
|
|
"""候选 provider 组合及是否命中缓存"""
|
|
|
|
|
|
|
|
|
|
|
|
provider: Provider
|
|
|
|
|
|
endpoint: ProviderEndpoint
|
|
|
|
|
|
key: ProviderAPIKey
|
|
|
|
|
|
is_cached: bool = False
|
|
|
|
|
|
is_skipped: bool = False # 是否被跳过
|
|
|
|
|
|
skip_reason: str | None = None # 跳过原因
|
|
|
|
|
|
mapping_matched_model: str | None = None # 通过映射匹配到的模型名(用于实际请求)
|
|
|
|
|
|
needs_conversion: bool = False # 是否需要格式转换
|
|
|
|
|
|
provider_api_format: str = "" # Provider 端点实际格式(用于健康度/熔断 bucket)
|
2026-02-19 10:40:49 +08:00
|
|
|
|
output_limit: int | None = None # GlobalModel 配置的模型输出上限
|
2026-02-28 11:44:08 +08:00
|
|
|
|
capability_miss_count: int = 0 # COMPATIBLE 能力不匹配数(0=完全匹配,用于排序)
|
2026-02-15 16:32:23 +08:00
|
|
|
|
|
|
|
|
|
|
def _stable_order_key(self) -> tuple[int, int, str, str, str]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
为排序/优先队列提供稳定的比较键。
|
|
|
|
|
|
|
|
|
|
|
|
说明:
|
|
|
|
|
|
- 运行时偶发会出现对 ProviderCandidate 做 tuple 排序/heap 排序的场景;
|
|
|
|
|
|
当主键相同需要比较候选本身时,若候选不可比较会触发:
|
|
|
|
|
|
TypeError: '<' not supported between instances of 'ProviderCandidate' and 'ProviderCandidate'
|
|
|
|
|
|
- 这里提供一个与调度逻辑无关、但足够稳定且可比的兜底顺序。
|
|
|
|
|
|
"""
|
|
|
|
|
|
provider_priority_raw = getattr(self.provider, "provider_priority", None)
|
|
|
|
|
|
internal_priority_raw = getattr(self.key, "internal_priority", None)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
provider_priority = (
|
|
|
|
|
|
int(provider_priority_raw) if provider_priority_raw is not None else 999999
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
provider_priority = 999999
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
internal_priority = (
|
|
|
|
|
|
int(internal_priority_raw) if internal_priority_raw is not None else 999999
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
internal_priority = 999999
|
|
|
|
|
|
|
|
|
|
|
|
provider_id = str(getattr(self.provider, "id", "") or "")
|
|
|
|
|
|
endpoint_id = str(getattr(self.endpoint, "id", "") or "")
|
|
|
|
|
|
key_id = str(getattr(self.key, "id", "") or "")
|
|
|
|
|
|
return (provider_priority, internal_priority, provider_id, endpoint_id, key_id)
|
|
|
|
|
|
|
|
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
|
|
|
|
if not isinstance(other, ProviderCandidate):
|
|
|
|
|
|
return NotImplemented
|
|
|
|
|
|
return self._stable_order_key() < other._stable_order_key()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-03 17:24:22 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class PoolCandidate(ProviderCandidate):
|
|
|
|
|
|
"""号池候选。
|
|
|
|
|
|
|
|
|
|
|
|
排序阶段作为单个候选参与;执行阶段再在 pool_keys 内部选择/切换 key。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
pool_keys: list[ProviderAPIKey] = field(default_factory=list)
|
|
|
|
|
|
pool_config: PoolConfig | None = None
|
|
|
|
|
|
pool_priority: int = 999999
|
|
|
|
|
|
_pool_key_index: int = 0
|
2026-03-09 13:26:54 +08:00
|
|
|
|
# 延迟可用性检查参数(号池优化:先排序再分页检查)
|
|
|
|
|
|
_deferred_check_params: dict[str, Any] | None = field(default=None, repr=False)
|
2026-03-03 17:24:22 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-15 16:32:23 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class ConcurrencySnapshot:
|
|
|
|
|
|
key_current: int
|
|
|
|
|
|
key_limit: int | None
|
|
|
|
|
|
is_cached_user: bool = False
|
|
|
|
|
|
# 动态预留信息
|
|
|
|
|
|
reservation_ratio: float = 0.0
|
|
|
|
|
|
reservation_phase: str = "unknown"
|
|
|
|
|
|
reservation_confidence: float = 0.0
|
|
|
|
|
|
load_factor: float = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
def describe(self) -> str:
|
|
|
|
|
|
key_limit_text = str(self.key_limit) if self.key_limit is not None else "inf"
|
|
|
|
|
|
reservation_text = f"{self.reservation_ratio:.0%}" if self.reservation_ratio > 0 else "N/A"
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"key={self.key_current}/{key_limit_text}, "
|
|
|
|
|
|
f"cached={self.is_cached_user}, "
|
|
|
|
|
|
f"reserve={reservation_text}({self.reservation_phase})"
|
|
|
|
|
|
)
|