diff --git a/research-wiki/designs/2026-07-20-m1-core-design.md b/research-wiki/designs/2026-07-20-m1-core-design.md index 31a849d..7dba1b3 100644 --- a/research-wiki/designs/2026-07-20-m1-core-design.md +++ b/research-wiki/designs/2026-07-20-m1-core-design.md @@ -190,7 +190,7 @@ class ProviderProfile: 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(profile)` 公开,新 provider 一个条目零核心改动(D11)。 +首发注册:`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 键名全集) diff --git a/research-wiki/plans/2026-07-20-m1-core-plan.md b/research-wiki/plans/2026-07-20-m1-core-plan.md index bcc7fe5..212b464 100644 --- a/research-wiki/plans/2026-07-20-m1-core-plan.md +++ b/research-wiki/plans/2026-07-20-m1-core-plan.md @@ -4,6 +4,7 @@ > **方案概述**: 严格按已批准设计 `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 先红后绿输出即测试证据。 +> **前置状态**: 设计 §13 的 7 项 ARCHITECTURE 反哺修订**已于开工前全部落地**(commit 2fb968b),执行者无需重做;唯 §8 模块图补 `config.py` 留在 T15。 ## 0. 文件结构(锁定分解) @@ -112,7 +113,7 @@ class ProviderGate(Protocol): ### 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 中立铁律)。 +- [ ] `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(全空)。注册表形态定稿(**无可变全局状态**,纯 asyncio 中立铁律): 模块级 `DEFAULT_PROFILES: Mapping[str, ProviderProfile]`(不可变);`register_provider(profile: ProviderProfile, *, base: Mapping[str, ProviderProfile] | None = None) -> dict[str, ProviderProfile]` 为**纯函数**,返回 base(缺省 DEFAULT_PROFILES)+ 新条目的新表,同名覆盖;`GatewayClient`/`from_env`/`from_settings` 收 `registry: Mapping[str, ProviderProfile] | None = None`(None → DEFAULT_PROFILES)。禁止运行时修改共享 dict。 - 测试: 三 profile 内容断言;未注册名报错;自定义 profile 注册后可查。 - 保真: 注入片段比对 VT `llm.py:130-144` 与 CHS `invokers.py:230-238`(三态语义是设计定稿的统一,允许双向覆盖)。 - 提交: `feat: add explicit provider profile registry` @@ -137,7 +138,7 @@ class ProviderGate(Protocol): ### T7 `sources.py` 选源与冷却(设计 §2.3/多源拍板) -- [ ] `RoundRobinSelector`(内部游标轮转起点)与 `LeastInflightSelector`(按 inflight 升序)——逐字移植 CHS `selector.py:20,36`;`SourceCooldownMemo`(dict[str, float] 冷却截止,`set_until/active/skip_reason`,时钟注入)。 +- [ ] `RoundRobinSelector`(内部游标轮转起点)与 `LeastInflightSelector`(按 inflight 升序)——逐字移植 `reference/CHSAnalyzer/app/providers/selector.py:20,36`(注意在 providers/ 不在 coordination/);`SourceCooldownMemo`(dict[str, float] 冷却截止,`set_until/active/skip_reason`,时钟注入)。 - 测试: 轮转顺序推进、least_inflight 排序稳定性、冷却备忘过期恢复。 - 提交: `feat: add source selectors and cooldown memo` @@ -156,7 +157,7 @@ class ProviderGate(Protocol): - [ ] 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 过期。 +- 测试: key 稳定性(同请求同 key)、隔离性(model/namespace/salt/多模态字节任一变化 → key 变)、大 base64 不进 canonical_json(性能语义: digest 后长度恒定)、命中路径 structured 重建与 schema 变更重校验失败回源(**用 fake StructuredOutputStrategy**——Protocol 已在 T2 冻结,不被 T11 阻塞)、后端炸时读写降级、TTL 过期。 - 保真: 降级与序列化行为比对 VT `redis_cache.py`;key 公式是设计声明的替换(旧缓存冷启动已记迁移文档)。 - 提交: `feat: add response cache middleware with poisoning-safe keys` @@ -180,7 +181,7 @@ class ProviderGate(Protocol): - [ ] `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 键名全集注释模板)。 +- [ ] 解除 import-linter 门控(Makefile 去掉跳过逻辑),契约按 ARCH §8 生效:实现层(transports/backends/telemetry/structured)改为**独立层**(`|` 分隔,互不依赖),确需的边显式放行(如 transports→providers 读 ProviderProfile);`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) @@ -198,7 +199,7 @@ class ProviderGate(Protocol): > **前置(人类输入)**: 真实网关 `.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` 等)装配成功。 +- [ ] `tests/e2e/test_compat_govdoc.py` / `test_compat_videotree.py`: 只读 import 两项目 Protocol 断言 isinstance——导入方式: `sys.path.insert` 指向 `reference/GovDoc-SaaS/packages/docagent-core/src` 与 `reference/Video-Tree-TRM5`,失败(包 `__init__` 连带依赖装不上)则兜底为**按源文件逐字复制 Protocol 定义**进测试文件做结构断言(复制来源注明 文件:行号);按其调用点形态(`session_id/parent_call_id/cache_salt`)真实调用一次;用 VT 现有键名(`LLM_TIMEOUT` 等)装配成功。 - 验证: e2e 全 PASS,`tests/outputs/` 有产物(不提交)。 - 提交: `test: add real-gateway and dual-project onboarding smoke` @@ -215,7 +216,7 @@ class ProviderGate(Protocol): |---|---| | 签名实现时发现设计矛盾 | 停下回人类门,先修设计再继续;严禁现场改签名 | | 远程 Redis/网关凭据未就绪 | T13/T14 前向人类索取;其余任务不阻塞 | -| import-linter 契约与实际 import 冲突 | T12 才解除门控,前序任务自觉遵守层次,T12 一次清算 | +| import-linter 契约与实际 import 冲突 | Makefile 门控条件是 `polygateway.ports` 可导入——**T2 起 lint-imports 即自动生效**,属预期而非异常;T12 收紧实现层为独立层并一次清算 | | 覆盖率不达 80% | 缺口集中在错误分支——按 T5/T8 翻译表与异常分派逐行补 | 保真校验总注: 本计划 T3/T5/T6/T8/T9/T10/T11 均涉及 ARCH §1.4 移植蓝本,各任务已标注比对文件;凡与蓝本语义有出入之处必须能指到设计 §9 审计表的对应行(保留/替换/放弃),否则视为未声明的隐式丢弃。