docs: add M1 implementation plan with fifteen verifiable tasks
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
# M1 核心里程碑实现计划
|
||||
|
||||
> **目标**(一句话): 落地 PolyGateway M1 全部交付物,使 GovDoc 与 Video-Tree 能用 `GatewayClient.from_env()` + `chat()` 替代各自 `GovernedLLMClient` 跑通真实治理调用。
|
||||
> **方案概述**: 严格按已批准设计 `designs/2026-07-20-m1-core-design.md`(签名冻结的唯一依据,下称"设计")与 ROADMAP §2 七步依赖序实施;内核类型先行冻结,叶子纯函数次之,再 transport → 内存后端与中间件 → 结构化 → 装配层,最后端到端验证。**任何与设计冲突之处以设计为准;想改签名必须回人类门。**
|
||||
> **技术**: Python 3.11 / httpx / pydantic(+pydantic-settings);optional extras: redis、json_repair;pytest(asyncio_mode=auto)。conda 环境 `PolyGateway`。
|
||||
> **流程**: 全部工作在 feature 分支 `feature/m1-core`;每任务一个 commit(用 `commit` skill);每个行为任务先写失败测试再实现,pytest 先红后绿输出即测试证据。
|
||||
|
||||
## 0. 文件结构(锁定分解)
|
||||
|
||||
```text
|
||||
src/polygateway/
|
||||
├── types.py # LLMResponse/ChatRequest/SourceConfig/Usage/SourceStats/
|
||||
│ # TransportResult/RetryPolicy/BreakerConfig/BackpressurePolicy/GlobalLimits
|
||||
├── errors.py # 设计 §3 全部错误类
|
||||
├── ports.py # 全部 Protocol + Permit + Decision/Update(含 __post_init__ 校验)
|
||||
├── streaming.py # 三层活性看门狗(移植 VT)
|
||||
├── providers.py # ProviderProfile 注册表(qwen/deepseek/openai)
|
||||
├── sources.py # RoundRobinSelector / LeastInflightSelector / SourceCooldownMemo
|
||||
├── transports/openai_compat.py # SSE 解析纯函数 + OpenAICompatTransport
|
||||
├── backends/memory/limiter.py # InMemoryLimiter + _MemoryPermit
|
||||
├── backends/memory/breaker.py # InMemoryGate(CHS gate 契约)
|
||||
├── backends/memory/cache.py # InMemoryCache(dict+TTL)
|
||||
├── backends/redis_cache.py # RedisCache(笨 KV,optional extra)
|
||||
├── middleware/base.py # Middleware/CallNext 类型 + 洋葱组装函数
|
||||
├── middleware/telemetry.py # TelemetryMW + TelemetryEmitter(全库唯一遥测调用点)
|
||||
├── middleware/cache.py # CacheMW(key 公式/命中重建 structured)
|
||||
├── middleware/structured.py # StructuredMW(D14 阶梯)
|
||||
├── middleware/retry.py # RetryMW(尝试编排:选源→熔断门→限流→transport)
|
||||
├── middleware/ratelimit.py # QuotaGate(permit 获取 + wait/fail_fast + 后端故障报错)
|
||||
├── middleware/breaker.py # BreakerGate(gate 进出 + 冷却备忘协作)
|
||||
├── structured/json_repair.py # JsonRepairStrategy(normalize 钩子)
|
||||
├── structured/native_schema.py # NativeSchemaStrategy
|
||||
├── telemetry/sqlite.py # SQLiteRecorder(18 列)
|
||||
├── config.py # GatewaySettings + 多源/韧性键解析与校验(新增模块,任务 T12 同步补 ARCH §8 模块图)
|
||||
└── client.py # GatewayClient + from_env/from_settings + gather_bounded
|
||||
tests/
|
||||
├── unit/… # 每个 src 模块一个 test_ 文件
|
||||
├── contracts/test_limiter_contract.py / test_breaker_contract.py # 参数化后端 fixture
|
||||
├── integration/… # 远程 Redis / SQLite 并发 / 治理组合 / 取消穿透
|
||||
└── e2e/… # 真实网关冒烟 + GovDoc/VT 兼容冒烟(输出落 tests/outputs/)
|
||||
```
|
||||
|
||||
跨任务消费的冻结签名(设计 §2-§4 为全文,此处摘执行者最常引用的三组):
|
||||
|
||||
```python
|
||||
# ports.py —— 洋葱与每次尝试的接缝
|
||||
CallNext = Callable[[ChatRequest], Awaitable[LLMResponse]]
|
||||
class Middleware(Protocol):
|
||||
async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse: ...
|
||||
class Transport(Protocol):
|
||||
async def complete(self, *, messages: list[dict[str, Any]], source: SourceConfig,
|
||||
stream: bool, overlay: dict[str, Any], call_id: str) -> TransportResult: ...
|
||||
```
|
||||
|
||||
```python
|
||||
# ports.py —— 限流契约(CHS limiter.py 形态)
|
||||
class Permit(Protocol):
|
||||
async def release(self) -> None: ... # 幂等
|
||||
async def settle(self, actual_tokens: int) -> None: ... # 幂等,delta 结算
|
||||
class RateLimiter(Protocol):
|
||||
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: ...
|
||||
```
|
||||
|
||||
```python
|
||||
# ports.py —— 熔断契约(CHS provider_gate.py 形态,时间一律秒)
|
||||
class ProviderGate(Protocol):
|
||||
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: ...
|
||||
```
|
||||
|
||||
`GateDecision(source_name, allowed, state, epoch, is_probe, probe_owner, retry_after_s)`、`GateUpdate(applied, state, epoch, failure_count, retry_after_s)`,`__post_init__` 校验照 CHS `ports.py:405-455` 逐条移植。
|
||||
|
||||
---
|
||||
|
||||
## 任务清单
|
||||
|
||||
### T0 分支与骨架
|
||||
|
||||
- [ ] `git checkout -b feature/m1-core`;创建 §0 全部目录与空 `__init__.py`(含 `tests/contracts/`);`src/polygateway/py.typed` 占位。
|
||||
- 验收: `conda run -n PolyGateway python -c "import polygateway"` 通过;`make ci` 仍绿。
|
||||
- 提交: `chore: scaffold m1 module skeleton`
|
||||
|
||||
### T1 `types.py` + `errors.py`(设计 §2.1-§2.3 / §3)
|
||||
|
||||
- [ ] 实现设计 §2.1 `LLMResponse`(11 旧字段逐字保序无默认 + 4 新字段带默认)、§2.2 `ChatRequest`(chat() 参数全集 + `overlay: dict`,frozen)、§2.3 `SourceConfig`(字段/默认/三态 enable_thinking/missing_done/trust_env + `__post_init__` 不变式: `0 < inter < ttft < timeout_s`、`tpm > 0 → est_tokens > 0`、name/provider/base_url/api_key/model 非空)、`Usage`/`SourceStats`/`TransportResult`、韧性四件套(`RetryPolicy/BreakerConfig/BackpressurePolicy/GlobalLimits`,构造校验正数)。全部 frozen dataclass,中文 docstring。
|
||||
- [ ] `errors.py`: 设计 §3 类层级逐字实现;`GatewayUnavailableError` 校验(scope 非空、reason ∈ scope 级 5 值、retry_after_s ≥ 0);`ResultInvalidError` 携 `raw_text/repair_error/validation_errors`。
|
||||
- 测试(先红后绿): `tests/unit/test_types.py`——LLMResponse 位置构造 11 参兼容(模拟三项目 fake)、frozen 不可变、SourceConfig 各不变式逐条触发 ValueError;`tests/unit/test_errors.py`——reason 值域拒绝、retry_after_s 负数拒绝、继承关系(`isinstance(CircuitOpenError(...), GatewayUnavailableError)`)。
|
||||
- 验证: `conda run -n PolyGateway pytest tests/unit/test_types.py tests/unit/test_errors.py -v` 全 PASS。
|
||||
- 保真: SourceConfig 不变式对照 CHS `config.py:66-82`;错误构造形态对照 CHS `errors.py:82-176`。
|
||||
- 提交: `feat: add frozen core types and error taxonomy`
|
||||
|
||||
### T2 `ports.py`
|
||||
|
||||
- [ ] §0 三组签名 + `CacheBackend`(get/set)、`TelemetryRecorder`(`record_llm_call` 18 字段 keyword-only,字段名单见设计 §4.4)、`SourceSelector`(`order(sources, stats)`)、`StructuredOutputStrategy`(`request_overlay(schema) -> dict` / `parse(text) -> Any`)、`GateDecision/GateUpdate` 数据类。全部 `@runtime_checkable`。`ports.py` 只 import `types.py`/`errors.py`/标准库。
|
||||
- 测试: `tests/unit/test_ports.py`——最小 dummy 实现通过各 Protocol 的 isinstance;GateDecision `__post_init__` 全部非法组合逐条拒绝(OPEN 准入 / HALF_OPEN 非探针准入 / 探针缺 owner / 非探针带 owner / 负 epoch / 负 retry_after_s)。
|
||||
- 保真: GateDecision 校验逐段比对 CHS `ports.py:405-440`。
|
||||
- 提交: `feat: freeze all port protocols and gate snapshots`
|
||||
|
||||
### T3 `streaming.py` 看门狗(ROADMAP 2a)
|
||||
|
||||
- [ ] 移植 VT `adapters/streaming.py`(与 GovDoc 逐字节一致): `StreamLivenessTimeout(kind, elapsed_s, first_token_seen)` + `stream_with_liveness_timeouts(source, *, ttft_s, inter_token_s, total_s)`。只包裹单次 `__anext__`;`asyncio.timeout` 的 `expired()` 区分本层 deadline;finally `aclose` 底层迭代器。
|
||||
- 测试: `tests/unit/test_streaming.py`——ttft/inter_token/total 三层分别用慢迭代器触发(小超时值,真实时钟);正常流原样透传;上游 TimeoutError 不被误吞为本层超时;消费方取消时底层迭代器被 aclose。
|
||||
- 保真: 逐段比对 VT `adapters/streaming.py`;语义零改动。
|
||||
- 提交: `feat: port three-layer stream liveness watchdog`
|
||||
|
||||
### T4 `providers.py` 注册表(ROADMAP 2b)
|
||||
|
||||
- [ ] `ProviderProfile`(设计 §7)+ 模块级注册表 dict + `get_provider(name)`(未注册抛 `RequestRejectedError` 语义的装配错误——用 `ValueError`,装配期即炸)+ `register_provider(profile)`。首发三条目: qwen(`{"enable_thinking": True}`/`{"enable_thinking": False}`,strip_think_tags=True)、deepseek(`{"thinking": {"type": "enabled"}}`/`{"thinking": {"type": "disabled"}}`)、openai(全空)。注册表实例归属装配层持有(`from_env` 默认用模块级只读表)——**无可变全局状态**: `register_provider` 返回新表或要求显式表实例,禁止运行时改共享 dict(纯 asyncio 中立铁律)。
|
||||
- 测试: 三 profile 内容断言;未注册名报错;自定义 profile 注册后可查。
|
||||
- 保真: 注入片段比对 VT `llm.py:130-144` 与 CHS `invokers.py:230-238`(三态语义是设计定稿的统一,允许双向覆盖)。
|
||||
- 提交: `feat: add explicit provider profile registry`
|
||||
|
||||
### T5 `transports/openai_compat.py`(ROADMAP 3)
|
||||
|
||||
- [ ] SSE 纯函数移植: `_sse_data_payload`/`_sse_delta`(usage 帧旁路;content 与 reasoning_content 区分)/`_iter_sse_deltas`(`[DONE]` 检测、畸形 JSON 抛 SSE 异常)——蓝本 VT `llm.py:51-124`。
|
||||
- [ ] `OpenAICompatTransport`: 每源持有预配 httpx.AsyncClient(Bearer/超时/trust_env);请求体 = model/messages/stream(+`stream_options.include_usage` 仅流式)+ overlay 合并 + profile thinking 注入;流式路径走看门狗(ttft/inter/total 取自 SourceConfig,None 回退 timeout_s;**任何增量含 reasoning 都算 token**);非流式快路径(`stream=False`,单 JSON 响应,仅 total 超时);`<think>` 剥离按 profile;usage 缺失按 `est_tokens` 兜底并标 `estimated`(est_tokens=0 则 0+estimated);缺 `[DONE]`: `missing_done="retry"` 抛瞬时 / `"salvage"` 用已收内容且强制 estimated;零内容 early_eof 恒抛瞬时。
|
||||
- [ ] 错误翻译(设计引用 ARCH §6.2 全表): httpx Timeout/Transport 族、SSE 异常、看门狗超时 → `TransientError`;429 body `insufficient_quota`/401/403 → `SourceDeadError`;429 其余 → `TransientError(retry_after_s=秒数解析)`;400 → `RequestRejectedError`;≥500 → `TransientError`;其余 4xx → `RequestRejectedError`。
|
||||
- 测试: `tests/unit/test_openai_compat.py` 用 `httpx.MockTransport` + 从三项目真实响应二次构造的 SSE 帧样本——正常流(含 usage 帧/reasoning 帧)、畸形帧、缺 DONE 两种策略、early_eof、非流式、每条翻译规则逐行断言异常类型与 retry_after_s;`<think>` 剥离;usage 兜底与 usage_source。
|
||||
- 保真: SSE 逐段比对 VT `llm.py`;翻译逐段比对 CHS `invokers.py:127-227`;Retry-After 仅秒数形态(`invokers.py:127-141`)。
|
||||
- 提交: `feat: add openai-compatible transport with sse parsing and error translation`(可拆 2 个 commit: 纯函数先行)
|
||||
|
||||
### T6 `backends/memory/` limiter + breaker + 契约测试(ROADMAP 4b)
|
||||
|
||||
- [ ] `InMemoryLimiter`: 构造 `(scope, sources: dict[str, SourceConfig], global_limits: GlobalLimits, lease_ttl_s, now=time.monotonic)`;六道闸(全局/单源 × 并发/RPM/TPM,0 = 不启用);并发 = 带 lease_id+过期时刻的租约表(acquire 时惰性清理过期租约);RPM/TPM = 分钟滑动窗口计数;TPM 预扣 est、`settle(actual)` delta 结算可退款;`try_acquire` 任一闸满返 None **零副作用**;`acquire` 轮询 + `asyncio.sleep`(可取消);`mark_progress/progress_age_s`。单事件循环内的并发安全靠"检查-占用"同步完成(无 await 穿插),不引入锁。
|
||||
- [ ] `InMemoryGate`: CHS gate 契约的进程内实现——CLOSED/OPEN/HALF_OPEN 状态机、连续失败阈值、force_open、冷却到期半开**单探针**(带 probe_ttl_s 租约,持有者死亡后过期回收)、epoch 递增与 fencing(旧 epoch 写回 `applied=False`)、`release_probe` 幂等、`retry_after_s(sources)` 取最早可尝试时刻。时钟构造注入。
|
||||
- [ ] 契约测试 `tests/contracts/`: limiter——移植 CHS `tests/contracts_limiter.py` 5 项(并发占满/RPM 不归还/TPM 预扣结算退款/release 幂等/progress)+ 新增 settle 幂等、try_acquire 失败零副作用、租约过期回收;breaker——阈值开路/冷却半开单探针(第二个 try_enter 被拒)/探针成功闭路/探针失败重开/force_open 一击/探针租约过期后新探针可入/epoch fencing 拒迟到写回/release_probe 幂等。**fixture 参数化 backend**(M1 仅 memory,M2 加 redis 零改测试)。
|
||||
- 保真: 状态机比对 VT `breaker.py`(含 `_half_open_inflight` 语义升级为租约)与 CHS `provider_gate.py`;limiter 语义比对 CHS `limiter.py`(`_RedisPermit` 的 released/settled 幂等 flag、settle 落 acquire 窗口)。
|
||||
- 验证: `conda run -n PolyGateway pytest tests/contracts/ -v` 全 PASS。
|
||||
- 提交: `feat: add in-memory limiter and breaker satisfying backend contracts`
|
||||
|
||||
### T7 `sources.py` 选源与冷却(设计 §2.3/多源拍板)
|
||||
|
||||
- [ ] `RoundRobinSelector`(内部游标轮转起点)与 `LeastInflightSelector`(按 inflight 升序)——逐字移植 CHS `selector.py:20,36`;`SourceCooldownMemo`(dict[str, float] 冷却截止,`set_until/active/skip_reason`,时钟注入)。
|
||||
- 测试: 轮转顺序推进、least_inflight 排序稳定性、冷却备忘过期恢复。
|
||||
- 提交: `feat: add source selectors and cooldown memo`
|
||||
|
||||
### T8 `middleware/` 骨架 + RetryMW(ROADMAP 4a;D13 自研)
|
||||
|
||||
- [ ] `middleware/base.py`: `compose(middlewares, terminal) -> CallNext` 洋葱组装(外→内)。
|
||||
- [ ] `middleware/ratelimit.py` `QuotaGate`: 按 `quota_full`(wait/fail_fast)获取 permit;fail_fast 满时抛 `AllSourcesExhausted(reason="quota_exhausted")`;后端操作异常包为 `GovernanceBackendError`(**报错不放行**)。`middleware/breaker.py` `BreakerGate`: try_enter/写回/release_probe 封装,同样后端异常报错。
|
||||
- [ ] `middleware/retry.py` `RetryMW`: 每次尝试 = selector.order → 跳过冷却源(计 gate_rejections)→ `try_acquire`(None 跳过)→ `try_enter`(不准入: settle(0)+release、读 retry_after_s 写冷却备忘、计 gate_rejections)→ transport(新 call_id/uuid4)。异常分派(蓝本 CHS `governance.py:200-268` 逐段比对): `RequestRejectedError` 网关已应答记成功否则探针释放,上抛;`ResultInvalidError` **记成功**上抛;`CancelledError` 探针释放后重抛(永不吞);`SourceDeadError` force_open+立即换源;`TransientError` 记失败、actual=est 保守、退避 `min(base*2^n, max)*uniform(0.5,1.5)` 与 retry_after_s 取大后重试;尝试数达 `max_attempts`(=总尝试次数)抛 `AllSourcesExhausted(reason="retry_exhausted")`;全源被门拒 → `CircuitOpenError`(per_source_reasons 逐源填);源列表空 → `reason="no_sources"`。成功: settle(actual)+mark_progress+record_success。finally 嵌套保证 settle 后必 release;取消路径 settle(0)。逐次遥测经注入的 `TelemetryEmitter`。
|
||||
- 测试(fake transport/后端注入): 换源顺序、瞬时重试次数语义(max_attempts=3 → 恰 3 次尝试)、SourceDead 不退避立即换源、ResultInvalid 记成功且不重试、退避与 Retry-After 取大(注入 rng/sleep 断言时长)、取消穿透(cancel 后 permit 已释放、探针已还、CancelledError 冒出)、fail_fast 语义、per_source_reasons 内容、每次尝试 call_id 不同。
|
||||
- 保真: 循环结构逐段比对 CHS `governance.py:107-268`;退避公式比对 VT `llm.py`。
|
||||
- 验证: `conda run -n PolyGateway pytest tests/unit/test_retry.py -v` 全 PASS。
|
||||
- 提交: `feat: add retry middleware with per-attempt governance orchestration`
|
||||
|
||||
### T9 `middleware/cache.py` + 缓存后端(ROADMAP 4c)
|
||||
|
||||
- [ ] key 公式(ARCH §7.5): `pgw:cache:` + `sha256(canonical_json({model, messages_digest, namespace, salt}))`;`messages_digest`——文本 part 原文、多模态 part(含 `image_url` data URL)各自 sha256 后参与;namespace 取 per-call `cache_namespace` 覆盖装配默认,缺失时(启用缓存而无 namespace)装配期已报错;salt 仅非 None 参与(VT 旧键语义)。
|
||||
- [ ] `CacheMW`: get 命中 → 反序列化(未知字段过滤/缺字段吃默认)→ 若本次调用带 `structured` 用注入的 strategy 零网络重建 `structured_data`(失败按未命中 warning)→ 新 `cache_call_id` 构造 cache_hit=True 响应;未命中 → call_next → **成功且阶梯已通过**(StructuredMW 在内层,能返回即已通过)写缓存(排除 structured_data 字段)。get/set 异常一律 warning 降级。
|
||||
- [ ] `backends/memory/cache.py`(dict+过期时刻,测试用)与 `backends/redis_cache.py`(笨 KV: get/set(ttl),`redis` import 失败报"缺 extra"错误)。
|
||||
- 测试: key 稳定性(同请求同 key)、隔离性(model/namespace/salt/多模态字节任一变化 → key 变)、大 base64 不进 canonical_json(性能语义: digest 后长度恒定)、命中路径 structured 重建与 schema 变更重校验失败回源、后端炸时读写降级、TTL 过期。
|
||||
- 保真: 降级与序列化行为比对 VT `redis_cache.py`;key 公式是设计声明的替换(旧缓存冷启动已记迁移文档)。
|
||||
- 提交: `feat: add response cache middleware with poisoning-safe keys`
|
||||
|
||||
### T10 遥测: `telemetry/sqlite.py` + `middleware/telemetry.py`(ROADMAP 4d)
|
||||
|
||||
- [ ] `SQLiteRecorder`: 表 `llm_calls` 18 数据列 + created_at(旧 15 列名保留,`model_name`→`model` 更名,新增 `source_name/usage_source/cost`);WAL + busy_timeout=5000;单持久连接 + `threading.Lock`,写经 `asyncio.to_thread`;`INSERT OR IGNORE`;初始化/写入失败全降级 warning(`self._conn=None` 后续 no-op);`close()`。
|
||||
- [ ] `TelemetryEmitter`: **全库唯一** `record_llm_call` 调用点;`emit(request, *, response=None, error=None, call_id, source_name, latency_ms, ...)` 从请求与结果组装 18 字段;messages 落库前多模态 part 摘要(复用 T9 的 digest 函数);cost 恒 None(pricing M2)。`TelemetryMW`: 最外层,负责缓存命中与最终失败的记录;逐次记录由 RetryMW 持同一 Emitter 完成;取消路径 finally 尽力记 error="cancelled"。
|
||||
- 测试: 18 列 schema 断言、并发 50 协程写全落库、call_id 冲突幂等、db 路径不可写时全链路不抛、缓存命中记录(cache_hit=True, latency_ms=0)、失败记录 error 字段、取消记录;grep 断言 `record_llm_call(` 在 src/ 仅出现于 emitter(铁律执法测试)。
|
||||
- 保真: 比对 VT `telemetry.py`(连接管理/PRAGMA/降级);字段对照 ARCH §7.8。
|
||||
- 提交: `feat: add sqlite telemetry with single-emitter discipline`
|
||||
|
||||
### T11 结构化: strategies + `middleware/structured.py`(ROADMAP 5)
|
||||
|
||||
- [ ] `structured/json_repair.py` `JsonRepairStrategy(normalize=None)`: 围栏剥离(VT `_CODE_FENCE_RE` 蓝本)→ `json_repair.repair_json` → `json.loads` → normalize 钩子;`json_repair` import 失败报"缺 extra polygateway[structured]";失败抛 `ResultInvalidError(raw_text, repair_error)`。`structured/native_schema.py` `NativeSchemaStrategy`: `request_overlay(schema)` 产出 `{"response_format": {"type": "json_schema", "json_schema": {...}}}`;parse 复用修复链。
|
||||
- [ ] `StructuredMW`(设计 §5): 三档分派——不传直通;`"json"` 仅修复(dict/list 入 `structured_data`);pydantic 模型 = overlay 注入(profile 支持 native 时)→ call_next → 修复 → `model_validate`(**只形态**)→ 失败且余额 >0 则按反馈模板重问(原 messages + assistant 坏输出 + user 纠错,错误前 3 条各截 200 字符;重问叠加 NativeSchema)→ 耗尽抛 `ResultInvalidError(raw_text, repair_error, validation_errors)`。重问计数独立于 transport 重试;`max_structured_retries` 默认 1。
|
||||
- 测试: 三档行为;修好的脏 JSON(真实样本: 围栏/尾逗号/单引号);修不好的;校验失败→重问 messages 形态断言(模板逐字)→ 第二次成功;retries=0 直接抛(CHS 语义);重问经过 call_next(即照过内层计数,用 fake 断言调用次数);normalize 钩子生效。
|
||||
- 保真: 修复链比对 VT `loop.py:341-377`;`_normalize_action` **不入库**(设计 §5 细则 3)。
|
||||
- 提交: `feat: add structured output ladder with bounded feedback retries`
|
||||
|
||||
### T12 装配: `config.py` + `client.py`(ROADMAP 6)
|
||||
|
||||
- [ ] `config.py` `GatewaySettings`(pydantic-settings): 设计 §8 全部键——多源 `{SCOPE}__{PROVIDER}__{N}__{FIELD}`(FIELD 全集含 MISSING_DONE/TRUST_ENV;段数≠4 或 GLOBAL 段跳过;未知 FIELD/未知 provider/缺必填字段报错)、scope 全局闸、per-scope 韧性键、平铺 `LLM_*` 简写(scope 键优先)、`PGW_*` 装配键(启用缓存缺 `PGW_CACHE_NAMESPACE` 或 TTL≤0 报错)。装配守卫: `timeout_s ≤ lease_ttl`、有效熔断阈值 `max(threshold, concurrency*2)` 自动、`max_attempts ≥ 1`。
|
||||
- [ ] `client.py`: `GatewayClient`(构造函数全量注入;`chat()` 设计 §2.2 签名逐字;组装洋葱 遥测→缓存→结构化→重试;`aclose()` 幂等 + async context manager)、`from_env(scope, *, limiter/breaker/cache/telemetry 注入覆盖)`、`from_settings(settings, ...)`、`gather_bounded(aws, *, concurrency)`。`__init__.py` 顶层导出: GatewayClient、LLMResponse、全部错误类、SourceConfig、gather_bounded、register_provider。
|
||||
- [ ] 解除 import-linter 门控(Makefile 去掉跳过逻辑),契约按 ARCH §8 生效(必要时把 `sources.py`/`config.py` 归入正确层)。回填 `.env.example`(§8 键名全集注释模板)。
|
||||
- 测试: from_env 缺键逐个报错、多源聚合(两源解析)、平铺与 scope 键优先级、注入覆盖生效(传入 fake limiter 断言被使用)、两个 client 共享同一 InMemoryLimiter 时全局并发闸生效(合计 inflight 封顶)、chat() 端到端(全 fake 后端+MockTransport: 命中缓存/瞬时重试/结构化三档 各一条)、aclose 幂等、gather_bounded 保序与并发上限、`isinstance` 结构性满足 GovDoc/VT 的 LLMProvider Protocol(只读 import reference)。
|
||||
- 验证: `make ci` 绿(**首次含 import-linter**);`conda run -n PolyGateway pytest tests/ -v` 全 PASS。
|
||||
- 提交: `feat: add gateway client with env-driven assembly`(config 与 client 可拆 2 commit)
|
||||
|
||||
### T13 集成测试(需真实 Redis)
|
||||
|
||||
> **前置(人类输入)**: 实验室远程 Redis 连接串写入 `.env`(`REDIS_URL`)。测试 namespace `pgw:test:{uuid4}`,teardown 全清理,禁止触碰既有键。
|
||||
|
||||
- [ ] `tests/integration/`: 远程 Redis 缓存读写与 TTL、断连降级(错误 URL 注入 → get/set 静默、chat 照常)、SQLite 并发写、治理组合(内存后端: 熔断开路→冷却→半开探针恢复全链;限流 wait/fail_fast;换源)、取消穿透(mid-stream 与 mid-backoff cancel → permit/探针释放断言)。
|
||||
- 验证: `conda run -n PolyGateway pytest tests/integration/ -v` 全 PASS;覆盖率 `make test` ≥80%。
|
||||
- 提交: `test: add integration suite for redis cache and governance combos`
|
||||
|
||||
### T14 e2e 冒烟(需真实网关凭据)
|
||||
|
||||
> **前置(人类输入)**: 真实网关 `.env`(至少一个 `LLM__{PROVIDER}__1__*` 源)。
|
||||
|
||||
- [ ] `tests/e2e/test_smoke_gateway.py`: 流式/非流式/`structured="json"`/pydantic 模型 四条真实调用,结果 + 遥测行断言,结构化 Markdown 输出落 `tests/outputs/e2e/`。
|
||||
- [ ] `tests/e2e/test_compat_govdoc.py` / `test_compat_videotree.py`: 只读 import 两项目 Protocol 断言 isinstance;按其调用点形态(`session_id/parent_call_id/cache_salt`)真实调用一次;用 VT 现有键名(`LLM_TIMEOUT` 等)装配成功。
|
||||
- 验证: e2e 全 PASS,`tests/outputs/` 有产物(不提交)。
|
||||
- 提交: `test: add real-gateway and dual-project onboarding smoke`
|
||||
|
||||
### T15 收尾: 验证门与合并
|
||||
|
||||
- [ ] 派**全新上下文 verifier subagent**(`verification-before-completion`): 对照设计逐节 + ROADMAP §2 验收出口逐条,证据必须指向本会话工具输出。
|
||||
- [ ] 更新 ROADMAP §1 M1 状态、ARCH §8 模块图补 `config.py`;wiki 注册 plan 实体与 implements 边。
|
||||
- [ ] `finishing-a-development-branch` skill 走合并(合并前 `requesting-code-review`)。
|
||||
- 验证: `make ci` 绿;覆盖 ≥80%;验证报告无未闭合项。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
| 风险 | 对策 |
|
||||
|---|---|
|
||||
| 签名实现时发现设计矛盾 | 停下回人类门,先修设计再继续;严禁现场改签名 |
|
||||
| 远程 Redis/网关凭据未就绪 | T13/T14 前向人类索取;其余任务不阻塞 |
|
||||
| import-linter 契约与实际 import 冲突 | T12 才解除门控,前序任务自觉遵守层次,T12 一次清算 |
|
||||
| 覆盖率不达 80% | 缺口集中在错误分支——按 T5/T8 翻译表与异常分派逐行补 |
|
||||
|
||||
保真校验总注: 本计划 T3/T5/T6/T8/T9/T10/T11 均涉及 ARCH §1.4 移植蓝本,各任务已标注比对文件;凡与蓝本语义有出入之处必须能指到设计 §9 审计表的对应行(保留/替换/放弃),否则视为未声明的隐式丢弃。
|
||||
Reference in New Issue
Block a user