# M1 核心里程碑设计:公共签名冻结与治理栈落地

> **状态**: Claude 自审 ✅ → 独立审查 ✅ → 人类门 ✅(2026-07-20)→ 实现完成 → 独立验证 ✅(verifier 发现 1 Critical + 3 Important 均已处理)。
> **实现期勘误(2026-07-20,均经人类确认)**: ① **空补全分类**——服务 200 且流程完整但 content 为空(MiniMax 间歇形态)→ `TransientError("empty_completion")`,退避重试/换源,绝不缓存(承 CHS 零内容归瞬时先例;验证 e2e flaky 的根因修复);② `GatewaySettings` 实现为 frozen dataclass + python-dotenv(多源动态键族 pydantic-settings 无法建模;python-dotenv 显式入核心依赖),fail-loud 语义不变;③ `minimax` 基线 profile 入 DEFAULT_PROFILES(首个 register 之外的新增条目,D11 实战);④ §5 细则 2 升级策略实现为"全部源都声明 supports_native_schema 才启用 escalation"(混合支持的 scope 注入 response_format 会击穿不支持的源)。**依据**: ARCHITECTURE.md D1–D14 / ROADMAP §2 / migrations/ 三份;签名证据来自 2026-07-20 对 reference/ 三项目的逐字提取(文中 `文件:行号` 均指 `reference/` 下路径)。
> **范围**: M1 全部交付物(ROADMAP §2 七步),含 2026-07-20 人类拍板:多源完整行为进 M1;GovDoc 与 Video-Tree 双冒烟;Redis 集成测试用实验室远程实例。
> **本文档冻结的内容**: `types.py` / `errors.py` / `ports.py` 公共签名、配置键名、五个开放设计轴的取舍。与 ARCHITECTURE.md 冲突处在 §13 列为反哺修订,经人类批准后先改 ARCHITECTURE 再实施。

## 1. 目标与非目标

M1 出口:GovDoc 与 Video-Tree 用 `GatewayClient.from_env()` + `chat()` 替代各自 `GovernedLLMClient` 跑通一次真实治理调用;`make ci` 绿、覆盖 ≥80%。非目标:Redis 限流/熔断后端、背压 stall、Postgres 遥测、pricing、OCR、音频(M2/M3)。

## 2. 核心类型冻结(`types.py`)

### 2.1 `LLMResponse`

前 11 个字段与两参考项目**逐字一致**(VT `core/types.py:12-29` ≡ GovDoc `docagent_core/types.py:8-25`),顺序不变、无默认值——三项目测试中按位置构造 fake 的代码零改动。新增字段全部带默认值(ARCHITECTURE §5.1 稳定性约定①):

```python
@dataclass(frozen=True)
class LLMResponse:
    content: str; thinking: str; model: str; provider: str
    prompt_tokens: int; completion_tokens: int; latency_ms: int
    ttft_ms: float | None; max_inter_token_ms: float | None
    cache_hit: bool; call_id: str
    # —— 库新增(只增不删,必带默认值)——
    source_name: str = ""
    cost: float | None = None
    usage_source: str = "measured"      # "measured" | "estimated"
    structured_data: Any | None = None  # D14 阶梯通过后的解析产物
```

- `structured_data` 不参与缓存序列化(pydantic 实例不可 JSON 往返);缓存命中且调用方传了 `structured` 时,由 **CacheMW 复用装配时注入 StructuredMW 的同一 strategy 实例**对缓存 `content` 重跑阶梯②③(零网络)再填充——schema 变更后旧缓存自动重校验,失败按未命中处理并记 warning(命中路径不经过 StructuredMW,该职责必须显式落在 CacheMW)。
- 缓存反序列化防御:未知字段过滤、缺失新字段吃默认值(版本偏移不炸)。

### 2.2 `chat()` 签名与 `ChatRequest`

`chat()` 按 ARCHITECTURE §5.2 定稿,唯一细化是 `structured` 的类型:

```python
async def chat(
    self, messages: list[dict[str, Any]], *,
    session_id: str | None = None,
    parent_call_id: str | None = None,
    cache_salt: str | None = None,
    cache_namespace: str | None = None,
    structured: type[BaseModel] | Literal["json"] | None = None,
    stream: bool = True,
) -> LLMResponse: ...
```

洋葱内部流转 `ChatRequest`(frozen dataclass):上述全部参数 + `overlay: dict`(StructuredMW/注册表写入的请求体叠加项,如 response_format、thinking 参数)。中间件用 `dataclasses.replace()` 派生新请求,不做原地修改(见 §4.1 方案 A1)。

### 2.3 `SourceConfig`(CHS `config.py:43-83` 超集)与辅助类型

| 字段 | 类型/默认 | 说明 |
|---|---|---|
| name / provider / base_url / api_key / model | str,必填 | provider 必须是注册表键(§7) |
| max_concurrency / rpm / tpm | int = 0 | **0 = 该闸不启用**(VT/GovDoc 迁移无 TPM 配置) |
| est_tokens | int = 0 | TPM 预扣 + usage 缺失兜底;`tpm > 0` 时必填 > 0(装配期校验)。默认 0 时 usage 缺失兜底落 0 并标 `estimated`——与 §9 行 10 不矛盾:放弃的是 VT"缺失填 0 且**不标注**"的静默行为 |
| timeout_s | float,必填 | 须 ≤ permit 租约 TTL(ARCH §7.3 装配守卫) |
| ttft_timeout_s / inter_token_timeout_s | float \| None = None | 不变式 `0 < inter < ttft < timeout_s`(CHS `config.py:66-82`) |
| enable_thinking | bool \| None = None | **三态**:None=不注入(模型默认)/ True=注入开启 / False=注入关闭——同时覆盖 VT"注入开启"(`llm.py:130-144`)与 CHS"注入关闭"(`invokers.py:230-238`)两种现状 |
| missing_done | str = "retry" | SSE 缺 [DONE] 语义,见 §6 |
| trust_env | bool = True | 代理绕行(VT `ocr.py:46` 教训,M3 OCR 复用) |

### 2.4 client 公共面与装配签名

| 签名 | 说明 |
|---|---|
| `async def aclose(self) -> None` + `__aenter__/__aexit__` | 生命周期 API(ARCH §5.1 约定③);aclose 幂等,关闭各源 httpx client 与遥测连接 |
| `from_env(scope: str = "LLM", *, limiter: RateLimiter \| None = None, breaker: ProviderGate \| None = None, cache: CacheBackend \| None = None, telemetry: TelemetryRecorder \| None = None) -> GatewayClient` | classmethod;None 项按 env 构建私有实例,**显式传入即共享**——多逻辑角色共享全局并发/RPM/TPM 闸(VT `TREE_BUILD_API_CONCURRENCY` 语义,VT 迁移缺口 R5)= 业务侧自建一个 InMemoryLimiter 传给多次 `from_env`;全局闸计数在 limiter 实例内,单源闸按 source name 键 |
| `from_settings(settings: GatewaySettings, *, limiter=..., breaker=..., cache=..., telemetry=...) -> GatewayClient` | 同上,配置对象替代环境读取(`GatewaySettings` 为 pydantic-settings 模型,聚合 §8 全部键) |
| 构造函数 | 全量依赖注入(middlewares/transport/sources/registry/后端全显式),测试与高级用户路径 |
| `async def gather_bounded(aws: Iterable[Awaitable[T]], *, concurrency: int) -> list[T]` | 模块级便利函数;语义 = `asyncio.gather` 默认行为(结果保序、首个异常上抛)+ semaphore 并发上限;替代 VT 手搓样板 |

其余:`Usage(prompt_tokens, completion_tokens, usage_source)`;`SourceStats(inflight, rpm_used, tpm_used)`(CHS `ports.py:396`);`TransportResult`(transport 返回的内部类型:content/thinking/两 token 数/usage_source/ttft_ms/max_inter_token_ms/raw)。韧性配置四件套沿用 CHS 字段:`RetryPolicy(max_attempts, backoff_base_s, backoff_max_s)`、`BreakerConfig(fail_threshold, cooldown_s, probe_ttl_s)`、`BackpressurePolicy(stall_window_s, poll_interval_s)`(M1 仅 poll 用,stall 判定 M2)、`GlobalLimits(max_concurrency, rpm, tpm)`。

**`max_attempts` 语义统一**:= 总尝试次数(含首次)。VT `range(max_retries)` 同义;CHS `fails > max_attempts` 为失败数上限,迁移映射时 +1(记入 CHS 迁移文档)。

## 3. 错误模型冻结(`errors.py`)+ §6.1 勘误

```python
class PolyGatewayError(Exception):
    def __init__(self, message: str, *, source_name: str | None = None,
                 status_code: int | None = None, operation: str | None = None): ...

class TransientError(PolyGatewayError):        # + retry_after_s: float | None = None
class SourceDeadError(PolyGatewayError): ...
class RequestRejectedError(PolyGatewayError): ...
class ResultInvalidError(PolyGatewayError):
    # + raw_text: str, repair_error: str | None, validation_errors: tuple[str, ...]

class GatewayUnavailableError(PolyGatewayError):
    # + scope: str, reason: str, retry_after_s: float, per_source_reasons: dict[str, str]
class CircuitOpenError(GatewayUnavailableError): ...   # reason 恒 "circuit_open"
class AllSourcesExhausted(GatewayUnavailableError): ...
class GovernanceBackendError(PolyGatewayError): ...    # 限流/熔断后端故障:报错不放行
```

基类构造形态承 CHS `ProviderError`(`errors.py:82`);`GatewayUnavailableError.retry_after_s` 为**非可选 float**(CHS `ProviderUnavailableError:155-176` 同),业务侧 arq 延期重投(`workers/tracking.py:406-428`)只需 catch 基类。全部类自 `polygateway` 顶层导出(§5.1 约定②)。

**勘误(反哺 ARCHITECTURE §6.1)**:该节称 reason 承接"CHS 的 7 种:circuit_open/retry_exhausted/stalled/quota_exhausted/no_sources/backpressure_timeout/probe_pending",但 CHS `errors.py:143-153` 实际值域是 `{network_error, timeout, rate_limited, source_dead, circuit_open, retry_exhausted, stalled}`——前者混入了未曾存在的值。重组为两层值域:

| 层 | 值域 |
|---|---|
| scope 级 `reason` | circuit_open / retry_exhausted / stalled(M2 启用)/ quota_exhausted(fail-fast 限流满)/ no_sources |
| `per_source_reasons` 值 | network_error / timeout / rate_limited / source_dead / circuit_open / cooldown |

## 4. 端口冻结(`ports.py`,全部 `@runtime_checkable Protocol`)

### 4.1 设计轴 A:中间件上下文形态

`Middleware` 协议:`async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse`,`CallNext = Callable[[ChatRequest], Awaitable[LLMResponse]]`。

| 方案 | 权衡 |
|---|---|
| **A1 不可变 ChatRequest + 派生传递(推荐)** | 中间件互不污染;衍生请求(结构化反馈重问、overlay 注入)显式 `replace()`;观测数据由 LLMResponse/异常携带,逐次遥测在 RetryMW 内部(它本就拥有尝试编排的全部信息) |
| A2 可变 CallContext 贯穿 | 遥测取数方便,但任何层都能偷改状态,重问循环下请求突变不可追踪;三项目 God-method 的病根之一 |
| A3 contextvars 传递 | 隐式全局,违反纯 asyncio 中立铁律;直接否决 |

遥测调用点收敛:内部组件 `TelemetryEmitter`(包装 `TelemetryRecorder`,从 request+结果/异常组装 18 字段)注入 TelemetryMW 与 RetryMW,是全库**唯一**调用 `record_llm_call` 的地方(铁律执行点)。

### 4.2 设计轴 B:限流端口粒度

| 方案 | 权衡 |
|---|---|
| **B1 端口在 limiter 层,契约 = CHS 形态(推荐)** | `try_acquire/acquire/source_stats/mark_progress/progress_age_s` + `Permit(release, settle)`(CHS `ports.py:494,507`);算法在各后端内实现,**契约测试一套两后端共跑**(CHS `tests/contracts_limiter.py` 5 项扩充) |
| B2 端口在存储原语层(incr/zadd 抽象),算法一份在中间件 | Redis 六道闸必须单条 Lua 原子执行(§10 原子性),原语端口无法原子组合;否决 |
| B3 无端口,duck-type 两个类 | 失去 import-linter 可执法的接缝;否决 |

D3"算法只有一份"在限流处的准确含义 = **语义契约与契约测试只有一份**(ARCHITECTURE §7.3 已如此表述);熔断/重试/退避则是字面的算法一份。`settle()/release()` 幂等、permit 带 TTL 租约、TPM 预扣 `est_tokens` 结算多退少补,全部进契约测试。

### 4.3 设计轴 C:熔断端口

| 方案 | 权衡 |
|---|---|
| **C1 采 CHS gate 契约(推荐)**:`try_enter(source, owner) -> Decision`、`record_success(entry)`、`record_failure(entry, reason, force_open)`、`release_probe(entry)`、`retry_after_s(sources)`(`provider_gate.py:105-169`) | 半开探针租约(TTL 防探针死锁)与 epoch fencing(防旧世代写回)在**契约内**;M2 的 Redis 版零适配移植;内存版以 VT `breaker.py` 状态机为蓝本按此契约重写(时钟构造注入) |
| C2 采 VT 简单 API(`is_open/record_failure/force_open/record_success`) | 内存版零改动,但探针 owner 与 epoch 无处安放,M2 Redis 版被迫另立 API → 中间件写两套分支,违反算法一份 |
| C3 两套 API 并存各配各的 | 同上,直接否决 |

`Decision/Update` 数据类照 CHS `ports.py:405-455` 冻结(含 `__post_init__` 一致性校验):`Decision(source_name, allowed, state, epoch, is_probe, probe_owner, retry_after_s)`、`Update(applied, state, epoch, failure_count, retry_after_s)`;时间单位一律**秒**(与 `cooldown_s`、`GatewayUnavailableError.retry_after_s` 一致;CHS Redis 实现内部的毫秒换算是后端私事,不进契约)。有效阈值 `max(configured, concurrency*2)` 由装配层自动计算(三项目 .env 注释的手动约定入库)。

### 4.4 其余端口(无争议,直接冻结)

| 端口 | 签名要点 | 出处/理由 |
|---|---|---|
| `Transport` | `async def complete(*, messages, source: SourceConfig, stream: bool, overlay: dict, call_id: str) -> TransportResult`;内部含看门狗、SSE 解析、ARCH §6.2 错误翻译。**看门狗活性口径**:任何增量(content **或** reasoning_content)都算 token——ttft = 首个任意 token、思考流刷新 inter_token 计时(CHS `invokers.py:55-79`;CHS 迁移约束 R1,防 thinking 模型被看门狗误杀) | D2;overlay 承接注册表 thinking 参数与 NativeSchema 的 response_format |
| `CacheBackend` | `async get(key) -> str \| None` / `async set(key, value, ttl_s) -> None`;**key 公式上移至 CacheMW**(算法一份),后端是笨 KV | VT `redis_cache.py` 的 key 构造从类中析出;内存版 dict+TTL 供测试与无 Redis 场景 |
| `TelemetryRecorder` | `async record_llm_call(*, <18 字段>)`,keyword-only。字段冻结:`call_id, parent_call_id, session_id, model, provider, source_name, messages, response, thinking, prompt_tokens, completion_tokens, usage_source, latency_ms, ttft_ms, max_inter_token_ms, cache_hit, error, cost`(ARCH §7.8) | 唯一更名:旧 Protocol 的 `model_name` → `model`(VT 迁移文档 §8 已记录,grep 实测两项目均**只写不读** telemetry.db,更名零风险);表名 `llm_calls` 保留,旧 db 留档 |
| `SourceSelector` | `def order(sources, stats) -> list[SourceConfig]`;首发 round_robin / least_inflight | CHS `selector.py:12` 逐字 |
| `StructuredOutputStrategy` | `def request_overlay(schema: dict \| None) -> dict` + `def parse(text: str) -> Any`(失败抛 `ResultInvalidError`) | 双实现见 §5 |
| 时钟/随机注入 | 有状态组件构造收 `now: Callable[[], float]`、`sleep`、`rng`(CHS `governance.py:85-89` 形态) | P6 可测试性 |

## 5. 设计轴 D:结构化输出阶梯落位(D14 细化)

| 方案 | 权衡 |
|---|---|
| **D1 独立 `StructuredMW`,层序 遥测→缓存→结构化→重试→transport(推荐)** | 重问 = 再次调用 `call_next`(天然照过限流/熔断门、逐次遥测);缓存在其外 → 只缓存阶梯通过的最终响应(D14"不固化坏结果"自动成立);反馈改 messages 经 `replace()` 显式派生 |
| D2 循环写在 client.chat() 里(洋葱之上) | 重问绕过遥测/缓存中间件,需手工补记账;循环与洋葱两处编排,回到 God-method |
| D3 策略塞进 transport | transport 管协议不管语义;重问跨 attempt,与"transport 无重试"的 D2 职责划分冲突 |

细则(D14 留给本设计的三件事):

1. **反馈模板**(库内常量,英文、零业务词):重问 = 原 messages + `assistant(原始坏输出)` + `user("Your previous reply was not valid JSON matching the required schema. Errors: {errors}. Reply with ONLY the corrected JSON object.")`。`{errors}` 取修复/校验错误前 3 条、每条截断 200 字符(防 prompt 膨胀)。
2. **策略升级**:首次尝试用装配时策略(注册表声明 `supports_native_schema` 则 NativeSchema,否则 JsonRepair);重问一律叠加 NativeSchema(若支持)——修复失败说明 prompt 约定不够,升级到协议约束。
3. **归一化钩子**:VT/GovDoc 的 `_normalize_action`(`loop.py:379-399`,DeepSeek 平铺参数收拢)是**面向特定业务 schema 的**归一化,入库即违反零业务假设。库提供 `JsonRepairStrategy(normalize: Callable[[Any], Any] | None = None)` 注入点,业务侧自带归一化函数;`ResultInvalidError` 携带的 `raw_text` 保证兜底可行。

`structured="json"` 档 = 仅阶梯②(围栏剥离 → json_repair → `json.loads`),产物为 dict/list,失败抛 `ResultInvalidError` 不重问——恰好继任 CHS 被跨模块偷 import 的 `_extract_json_object`(`extractors.py:32-51`,其"失败即抛、不修复"语义由 `max_structured_retries=0` 默认覆盖)。`max_structured_retries` 默认 1,装配级配置;CHS 迁移设 0。

## 6. 设计轴 E:SSE 截断语义

现状分歧:VT 缺 `[DONE]` 一律 `_SseAnomaly("truncated_no_done")` 重试(`llm.py:585-586`);CHS 区分 early_eof(零内容,retry)/ missing_done(有内容,**打捞**)(`invokers.py:307-313`);GovDoc 无检查(缺陷)。

| 方案 | 权衡 |
|---|---|
| E1 一律 strict 重试 | 语义最干净,但 CHS 迁移丢打捞行为(迁移文档常驻约束:旧版行为不得隐式丢弃) |
| E2 一律打捞 | 截断响应可能进缓存被固化,违反缓存防毒化精神 |
| **E3 per-source `missing_done: "retry" \| "salvage"`,默认 retry(推荐)** | 默认严格(防缓存固化截断);CHS 迁移配 salvage 保留原行为;early_eof(零内容)恒 retry 不可配 |

打捞路径强制 `usage_source="estimated"`(usage 帧通常随 [DONE] 前的末帧丢失)。

## 7. provider 注册表(`providers.py`)

```python
@dataclass(frozen=True)
class ProviderProfile:
    name: str
    thinking_on: dict          # enable_thinking=True 时并入请求体
    thinking_off: dict         # enable_thinking=False 时并入
    strip_think_tags: bool     # 响应 content 中 <think> 剥离(qwen)
    supports_native_schema: bool = False
```

首发注册:`qwen`(`{"enable_thinking": True}` / `{"enable_thinking": False}`、剥 think 标签)、`deepseek`(`{"thinking": {"type": "enabled"}}` / `{"thinking": {"type": "disabled"}}`)、`openai`(全空,基线)。`reasoning_content` 增量提取是 OpenAI 兼容 SSE 的通用行为,归 transport 不进 profile。查找按 `SourceConfig.provider` **精确匹配**,未注册即装配期报错(消灭子串猜测);`register_provider` 公开,新 provider 一个条目零核心改动(D11)。**形态定稿(2026-07-20 计划审查修订)**: 纯函数 `register_provider(profile, *, base: Mapping | None = None) -> dict[str, ProviderProfile]`,返回 base(缺省模块级不可变 `DEFAULT_PROFILES`)+ 新条目的新表;client/from_env 收 `registry: Mapping | None = None`——无可变全局状态(铁律)。

## 8. 配置面定稿(from_env 键名全集)

| 组 | 键 | 说明 |
|---|---|---|
| 多源 | `{SCOPE}__{PROVIDER}__{N}__{FIELD}`,FIELD ∈ CHS 全集(`config.py:95-104`)+ `ENABLE_THINKING / TTFT_TIMEOUT_S / INTER_TOKEN_TIMEOUT_S / MISSING_DONE / TRUST_ENV` | 聚合为 `list[SourceConfig]` |
| scope 全局闸 | `{SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM` | CHS 同名 |
| per-scope 韧性 | `{SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S`;`{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S`;`{SCOPE}__BACKPRESSURE__STALL_WINDOW_S / POLL_INTERVAL_S`;`{SCOPE}__SELECTOR`;`{SCOPE}__QUOTA_FULL`(wait/fail_fast) | CHS 键名 + 新增 PROBE_TTL_S、QUOTA_FULL |
| 平铺简写(单 scope) | `LLM_TIMEOUT / LLM_MAX_RETRIES / LLM_RETRY_BASE_DELAY / LLM_RETRY_MAX_DELAY / LLM_CIRCUIT_BREAKER_THRESHOLD / LLM_CIRCUIT_BREAKER_COOLDOWN / LLM_TTFT_TIMEOUT / LLM_INTER_TOKEN_TIMEOUT` | VT/GovDoc 现有键零改名迁移;与 scope 键并存时 scope 键优先(ARCHITECTURE §9) |
| 装配选择 | `PGW_LIMITER_BACKEND` / `PGW_BREAKER_BACKEND`(memory\|redis,M1 仅 memory)、`PGW_CACHE_BACKEND`(redis\|memory\|none)、`PGW_TELEMETRY_BACKEND`(sqlite\|none)、`PGW_TELEMETRY_SQLITE_PATH`、`PGW_CACHE_NAMESPACE`(缓存启用时**必填**)、`PGW_CACHE_TTL_S`(必填 >0)、`PGW_STRUCTURED_MAX_RETRIES` | 缺关键键直接报错,严禁默认值兜底 |
| 复用 | `REDIS_URL` | VT 同名 |

`from_env(scope)` 按 scope 装配单 client;多逻辑角色 = 业务侧对每个角色调一次 `from_env(scope=...)`,共享状态后端经 §2.4 的显式注入参数传入(ARCH §7.7,VT 迁移 R5)。VT 的 `SEARCH_LLM_MODEL` 旧式键由其迁移文档映射为 `SEARCH__...`,库不做旧键兼容层。

## 9. 旧版行为审计(替换 VT/GovDoc `GovernedLLMClient` 栈 + CHS 治理循环)

| # | 旧行为(出处) | 处置 |
|---|---|---|
| 1 | chat() `cache_salt`(仅 VT,`llm.py:274`;调用点 `loop.py:336`) | 保留(GovDoc 无此参 = 超集兼容) |
| 2 | 熔断半开单探针(仅 VT `breaker.py:45-48` 内存 flag;GovDoc 每调用放行) | 替换为带 TTL 租约探针(C1 契约);GovDoc 无探针行为**有意放弃**(缺陷) |
| 3 | 熔断时钟逐调用传 `now`(VT/GovDoc) | 替换为构造注入 `now()`(等价可测,签名更稳) |
| 4 | 有效阈值 = max(threshold, concurrency*2)(.env 注释约定) | 保留,装配层自动算 |
| 5 | 遥测:VT 持久连接+Lock+close() / GovDoc 每写新建连接 | 采 VT 形态 + `asyncio.to_thread` 桥接;GovDoc 形态放弃 |
| 6 | 遥测初始化/写入失败降级不冒泡、`INSERT OR IGNORE` 幂等、表 `llm_calls` 15 列 | 全保留;列只增(`source_name/usage_source/cost`) |
| 7 | 缓存:sha256(model+messages[+salt])、TTL>0 校验、读写失败静默降级(VT `redis_cache.py`) | 语义保留;**key 公式替换**(必填 namespace + 多模态 part 先摘要)→ 迁移后旧缓存一次性冷启动(迁移文档已记);GovDoc 无 salt/无隔离放弃(缺陷) |
| 8 | 瞬时判定:VT 宽(`TimeoutException+TransportError`)/ GovDoc 窄 | 采宽集(GovDoc 窄集放弃——ConnectError 漏网即 bug) |
| 9 | 429 body `insufficient_quota` → SourceDead;Retry-After 仅秒数(CHS `invokers.py:144-166`) | 保留 |
| 10 | usage 缺失按 `est_tokens` 估算 + `usage_source` 标注(CHS `invokers.py:241`) | 保留;VT"缺失填 0"放弃 |
| 11 | thinking:VT/GovDoc 子串猜测+注入开启;CHS 配置驱动+注入关闭 | 替换为注册表 profile + `enable_thinking` 三态(§2.3/§7) |
| 12 | qwen `<think>` 剥离(`llm.py:147-164`)、`reasoning_content` 增量提取(三家同款) | 保留(前者 registry 声明,后者 transport 通用) |
| 13 | SSE:ping/空行跳过、usage 帧旁路、畸形 JSON → 瞬时、强制 `include_usage` | 保留(三家同源纯函数移植) |
| 14 | 缺 [DONE]:VT strict / CHS salvage / GovDoc 无检查 | E3 可配默认 strict;GovDoc 无检查放弃(缺陷) |
| 15 | 重试:退避公式 `min(base*2^n, max)*uniform(0.5,1.5)` 与 Retry-After 取大;每 attempt 新 call_id;致命 `force_open` 即抛 | 保留(D13);两家 jitter 系数差异按 VT 收敛 |
| 16 | CHS 循环:源冷却备忘、`gate_rejections==len(sources)` 判 circuit_open、`ResultInvalid` 记成功、finally 嵌套 settle→release、Cancel 时 release_probe(`governance.py:107-266`) | 全保留(RetryMW 蓝本) |
| 17 | 限流契约:预扣/结算/退款、release 幂等、mark_progress(CHS limiter + 契约测试 5 项) | 保留;M1 内存实现同契约 |
| 18 | 缓存命中独立 `cache_call_id` + 记遥测(VT `llm.py` 缓存分支) | 保留(cache_hit=True, latency_ms=0) |
| 19 | JSON 解析:VT 内 ≥5 套并存、失败语义不一 | 替换为 `JsonRepairStrategy` 单实现;业务归一化经 normalize 钩子外置(§5 细则 3) |
| 20 | 非流式路径:三家均无(硬编码 stream=True) | 新增 `stream=False` 快路径(单 JSON 响应,仅 total 超时) |
| 21 | `evolve_llm = llm` 隐式共享实例(VT `main.py`) | 放弃隐式;共享 = 显式注入同一状态后端(§2.4) |
| 22 | 看门狗活性:thinking 增量刷新计时、ttft=首个任意 token(CHS `invokers.py:55-79`) | 保留(§4.4 Transport 行;CHS 迁移 R1) |

M2 备忘(不入 M1):CHS 图片 magic bytes 探测 mime(`invokers.py:116-124`)随 VLM 多模态迁移移入库 transport。

## 10. 非功能维度(逐条)

| 维度 | 回答 |
|---|---|
| 并发与取消 | 同一 client 被 N 协程并发调用为标准形态:所有中间件无实例级可变请求状态(A1);内存 limiter 用 asyncio 原语,断言单事件循环。`CancelledError`:退避 sleep / 限流等待 / `__anext__` 全可取消;取消路径 finally 依次 `release_probe`(若探针)→ `permit.settle(0)+release()`(TPM 退款,承 CHS `governance.py` finally 语义)→ 尽力遥测(error="cancelled",写失败静默)→ 重抛;库内零 `except BaseException` |
| 降级方向 | 缓存 get/set 失败、遥测写失败 → warning 静默;limiter/breaker 后端操作失败 → `GovernanceBackendError` 上抛**不放行**(M1 内存后端仅内部 bug 会触发,契约照立,M2 Redis 直接继承) |
| 幂等与重复 | `settle/release/release_probe` 幂等(契约测试);遥测 call_id 主键 OR IGNORE;缓存同 key 重写同值无害 |
| 持久化与原子性 | SQLite WAL + busy_timeout,单持久连接 + to_thread,崩溃最多丢当次记录;缓存写在阶梯通过后(部分写入 = Redis 单 SET,原子);内存后端无持久化(进程死 = 状态清零,单进程语义下正确) |

## 11. 测试策略

| 层 | 内容 |
|---|---|
| unit(纯函数) | 看门狗(fake 迭代器+注入时钟,三层超时逐个触发)、SSE 解析(用三项目录制的真实帧样本二次构造,含畸形帧/usage 帧/缺 DONE)、退避公式、缓存 key 公式(namespace 隔离、salt、多模态摘要稳定性)、注册表、json_repair 链、错误翻译表逐行 |
| 契约(参数化后端) | limiter:CHS 5 项 + settle 幂等 + 装配守卫;breaker:状态机全迁移 + 探针租约过期回收 + force_open;M1 跑 memory,M2 同套跑 Redis |
| integration | 远程 Redis 缓存(命名空间 `pgw:test:{uuid}`,teardown 清理,断连模拟降级方向)、SQLite 并发写、RetryMW×限流×熔断组合(换源/冷却/circuit_open/取消穿透——`asyncio.Task.cancel` 注入到退避与流式中途) |
| e2e | 真实网关冒烟(流式/非流式/structured 三档),输出落 `tests/outputs/`;GovDoc 与 Video-Tree 双最小接入冒烟 |

每步先写失败测试再实现(测试结果门);行为变更的证据 = 本会话内 pytest 先红后绿输出。

## 12. 纸面兼容验证(调用点反推)

| 调用点 | 形态 | 兼容性 |
|---|---|---|
| VT `loop.py:336` | `chat(messages, session_id=, cache_salt=)` | ✅ 逐字兼容 |
| VT `summarizer.py:236` | `chat(messages, session_id=, parent_call_id=)` | ✅ |
| VT `evolve.py:566` | `chat(messages)` 纯位置 | ✅ |
| GovDoc `loop.py:377` | `chat(messages, session_id=, parent_call_id=)` | ✅;其 `LLMProvider` Protocol(`protocols.py:15-25`)为我方签名子集,结构性满足 |
| CHS `tracking.py:406-428` | catch `ProviderUnavailableError` → `exc.retry_after_s/scope/reason` | ✅ `GatewayUnavailableError` 字段同名(M2 接入) |
| CHS `classifiers.py:12` 偷 import `_extract_json_object` | → `structured="json"` 档 | ✅ 语义覆盖(不修复→修复是增强,失败仍抛异常) |

## 13. 对 ARCHITECTURE.md 的反哺修订(经人类批准后执行)

1. §6.1 reason 枚举勘误(本文档 §3):scope 级 5 值 + per_source 6 值,替换现文不实的 7 值清单。
2. §5.1 新增字段清单补 `structured_data`;§5.2 `chat()` 的 `structured` 参数类型定稿为 `type[BaseModel] | Literal["json"] | None`。
3. §7.1 补 SSE 缺 [DONE] 的 per-source `missing_done` 语义(本文档 §6,承 CHS salvage 迁移约束)。
4. §7.9 阶梯④补反馈模板与"重问叠加 NativeSchema"升级细则;归一化钩子外置声明。
5. §8 模块图 `middleware/` 清单补 `structured.py`(StructuredMW);`sources.py` 职责注明含选源策略(selector 不单设目录)。
6. §4.3 默认层序更新为 遥测→缓存→结构化→重试循环(StructuredMW 插入位,本文档 §5)。
7. §6.1 `AllSourcesExhausted`/`CircuitOpenError` 的 `retry_after_s` 定稿为**非可选 float**(替换现文 `float | None`;CHS 同款,0 表示"可立即重试")。
