feat: freeze all port protocols and gate snapshots

This commit is contained in:
2026-07-20 06:35:11 -04:00
parent 9f177d6d64
commit b46a62a8ae
2 changed files with 351 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
"""全部端口协议与治理快照类型(M1 设计 §4;最内层,只依赖 types/errors/标准库)。
决策逻辑与状态存储分离(D3): 中间件只面向这里的 Protocol,
后端(memory/redis)各自实现同一契约并共用一套契约测试。
时间量纲一律**秒**(CHS Redis 实现内部的毫秒换算是后端私事,不进契约)。
"""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any, Protocol, runtime_checkable
from .types import ChatRequest, LLMResponse, SourceConfig, SourceStats, TransportResult
CallNext = Callable[[ChatRequest], Awaitable[LLMResponse]]
@runtime_checkable
class Middleware(Protocol):
"""洋葱层: 包裹 call_next,层与层正交(D1)。"""
async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse: ...
@runtime_checkable
class Transport(Protocol):
"""一次原始调用的协议细节(请求组装/流式解析/错误翻译);不含任何治理。"""
async def complete(
self,
*,
messages: list[dict[str, Any]],
source: SourceConfig,
stream: bool,
overlay: dict[str, Any],
call_id: str,
) -> TransportResult: ...
@runtime_checkable
class Permit(Protocol):
"""限流入场许可;settle/release 均幂等,finally 中必然执行。"""
async def release(self) -> None: ...
async def settle(self, actual_tokens: int) -> None: ...
@runtime_checkable
class RateLimiter(Protocol):
"""限流契约(CHS limiter.py 形态): 并发/RPM/TPM × 全局/单源 六道闸。"""
async def try_acquire(self, source_key: str, est_tokens: int) -> Permit | None: ...
async def acquire(self, source_key: str, est_tokens: int) -> Permit: ...
async def source_stats(self, source_key: str) -> SourceStats: ...
async def mark_progress(self) -> None: ...
async def progress_age_s(self) -> float: ...
class GateState(StrEnum):
"""熔断状态机三态。"""
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
@dataclass(frozen=True)
class GateDecision:
"""一次源准入决定,同时是结果写回所需的 fencing token(CHS ports.py:405)。"""
source_name: str
allowed: bool
state: GateState
epoch: int
is_probe: bool
probe_owner: str | None
retry_after_s: float
def __post_init__(self) -> None:
# 校验逐条移植 CHS ports.py:405-440,保证快照内部一致
self._check_bounds()
self._check_admission()
self._check_probe()
def _check_bounds(self) -> None:
if not self.source_name.strip():
raise ValueError("source_name 不能为空")
if self.epoch < 0:
raise ValueError("epoch 不能为负")
if self.retry_after_s < 0:
raise ValueError("retry_after_s 不能为负")
def _check_admission(self) -> None:
if self.allowed and self.state is GateState.OPEN:
raise ValueError("OPEN 状态不得准入")
if self.allowed and self.state is GateState.HALF_OPEN and not self.is_probe:
raise ValueError("HALF_OPEN 准入必须是探针")
def _check_probe(self) -> None:
if self.is_probe:
probe_valid = (
self.allowed
and self.state is GateState.HALF_OPEN
and self.probe_owner is not None
and bool(self.probe_owner.strip())
)
if not probe_valid:
raise ValueError("探针决定必须同时包含准入、HALF_OPEN 与 owner")
elif self.probe_owner is not None:
raise ValueError("非探针决定不得携带 probe_owner")
@dataclass(frozen=True)
class GateUpdate:
"""门控写回快照;applied=False 表示 fencing 已拒绝迟到结果(CHS ports.py:442)。"""
applied: bool
state: GateState
epoch: int
failure_count: int
retry_after_s: float
def __post_init__(self) -> None:
if self.epoch < 0 or self.failure_count < 0 or self.retry_after_s < 0:
raise ValueError("门控更新快照不能包含负数")
@runtime_checkable
class ProviderGate(Protocol):
"""熔断契约(CHS provider_gate.py 形态): 半开单探针租约 + epoch fencing 在契约内。"""
async def try_enter(self, source_name: str, owner: str) -> GateDecision: ...
async def record_success(self, entry: GateDecision) -> GateUpdate: ...
async def record_failure(
self, entry: GateDecision, reason: str, force_open: bool
) -> GateUpdate: ...
async def release_probe(self, entry: GateDecision) -> GateUpdate: ...
async def retry_after_s(self, sources: tuple[str, ...]) -> float: ...
@runtime_checkable
class CacheBackend(Protocol):
"""响应缓存后端: 笨 KV;key 公式归 CacheMW(算法一份)。"""
async def get(self, key: str) -> str | None: ...
async def set(self, key: str, value: str, ttl_s: int) -> None: ...
@runtime_checkable
class SourceSelector(Protocol):
"""选源策略(CHS selector.py 形态): 返回本次尝试的候选顺序。"""
def order(
self, sources: list[SourceConfig], stats: dict[str, SourceStats]
) -> list[SourceConfig]: ...
@runtime_checkable
class StructuredOutputStrategy(Protocol):
"""结构化输出策略(D7/D14): 请求侧叠加 + 响应侧解析。"""
def request_overlay(self, schema: dict[str, Any] | None) -> dict[str, Any]: ...
def parse(self, text: str) -> Any: ...
@runtime_checkable
class TelemetryRecorder(Protocol):
"""遥测后端;18 字段冻结(M1 设计 §4.4),唯一调用点是 TelemetryEmitter。"""
async def record_llm_call(
self,
*,
call_id: str,
parent_call_id: str | None,
session_id: str | None,
model: str,
provider: str,
source_name: str,
messages: str,
response: str,
thinking: str,
prompt_tokens: int,
completion_tokens: int,
usage_source: str,
latency_ms: int,
ttft_ms: float | None,
max_inter_token_ms: float | None,
cache_hit: bool,
error: str | None,
cost: float | None,
) -> None: ...