docs: plan the tier work as ten steps that each stand on their own
Ordered so the two type changes land first and everything else consumes them: capability and wire in parallel, then the five gates, then the two entry points, then cache key and telemetry, then the transport. Task 10 exists because the human settled that the capability table is measured through new-api, not read off a vendor page. Task 1 lands the documented guess; task 10 replaces it with what the gateway does.
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
# 实现计划: 推理档位一等化
|
||||
|
||||
- **设计**: `research-wiki/designs/2026-09-04-reasoning-effort-design.md`(2026-09-04 人类已批准)
|
||||
- **目标**: 把 `enable_thinking: bool | None` 升级为可表达厂商档位的 `Effort` 词汇,让「关不掉的模型」「打空的档位」从静默失效变成带出路的报错。
|
||||
- **方案概述**: 新增八档封闭枚举 `Effort`(含 `auto`);能力表从 `can_disable: bool` 改为 `supported_efforts: tuple[Effort, ...]`;provider 的两个固定片段改为 `ThinkingWire`(off / on_base / effort_key);档位入口取「源级默认 + 请求级覆盖」,进缓存 key 与遥测各一列。
|
||||
- **涉及技术**: Python 3.12 `StrEnum`、frozen dataclass、pydantic-settings env 解析、SQLite/Postgres DDL 补列、pytest。
|
||||
- **保真校验**: **不适用**。本计划实现的是库自研的推理决策(`thinking.py` 系 2026-08-25 新建),不属 ARCHITECTURE §1.4 的移植蓝本;且 `reference/` 三项目当前不在工作区(见设计 §12),无可比对源。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 动作 | 职责 |
|
||||
|---|---|---|
|
||||
| `src/polygateway/types.py` | 修改 | 新增 `Effort` 枚举;`SourceConfig`/`ChatRequest` 各加档位字段 |
|
||||
| `src/polygateway/thinking.py` | 修改 | `ThinkingCapability` 重构、`resolve_thinking` 五关、`reconcile_thinking` 判据、默认能力表重写 |
|
||||
| `src/polygateway/providers.py` | 修改 | `ThinkingWire` 新类型替换两个片段;`DEFAULT_PROFILES` 扩到 8 段 |
|
||||
| `src/polygateway/config.py` | 修改 | 两个新 env 键的解析与矛盾校验 |
|
||||
| `src/polygateway/client.py` | 修改 | `chat()` 签名加档位;`_fingerprint_mark` 纳入源级档位 |
|
||||
| `src/polygateway/middleware/cache.py` | 修改 | `build_cache_key` 纳入请求级档位 |
|
||||
| `src/polygateway/middleware/telemetry.py` | 修改 | `_record` 与三个 emit 入口传递生效档位 |
|
||||
| `src/polygateway/ports.py` | 修改 | `TelemetryRecorder.record_llm_call` 加一参(25 → 26 字段) |
|
||||
| `src/polygateway/telemetry/schema.py` | 修改 | `COLUMNS`、两端 DDL、补列声明 |
|
||||
| `src/polygateway/telemetry/{sqlite,postgres}.py` | 修改 | 落库新列 |
|
||||
| `src/polygateway/transports/openai_compat.py` | 修改 | 生效档位解析接线、告警节流键 |
|
||||
| `src/polygateway/__init__.py` | 修改 | 导出 `Effort`、`ThinkingWire` |
|
||||
| `.env.example` | 修改 | 两个新键的模板与注释 |
|
||||
| `tests/unit/test_thinking.py` | 修改 | 位置参数构造迁移 + 五关用例 |
|
||||
| `tests/unit/test_providers.py` | 修改 | `ThinkingWire` 用例 |
|
||||
| `tests/unit/test_cache.py` | 修改 | 档位进 key 的用例 |
|
||||
| `tests/unit/test_openai_compat.py` | 修改 | transport 接线与节流用例 |
|
||||
| `tests/unit/test_telemetry.py`、`tests/integration/test_redis_cache.py` | 修改 | 列数断言与缓存 key 回归 |
|
||||
| `tests/e2e/test_thinking_live.py` | 修改 | `can_disable` 读法迁移;新增逐模型档位实测(标 `slow`) |
|
||||
|
||||
---
|
||||
|
||||
## 关键接口(跨任务消费,此处定稿)
|
||||
|
||||
```python
|
||||
# types.py
|
||||
class Effort(StrEnum):
|
||||
NONE = "none"; AUTO = "auto"; MINIMAL = "minimal"; LOW = "low"
|
||||
MEDIUM = "medium"; HIGH = "high"; XHIGH = "xhigh"; MAX = "max"
|
||||
|
||||
_ORDER = (Effort.NONE, Effort.MINIMAL, Effort.LOW, Effort.MEDIUM,
|
||||
Effort.HIGH, Effort.XHIGH, Effort.MAX) # auto 不参与强弱序
|
||||
```
|
||||
|
||||
```python
|
||||
# providers.py
|
||||
@dataclass(frozen=True)
|
||||
class ThinkingWire:
|
||||
off: Mapping[str, Any] | None
|
||||
on_base: Mapping[str, Any] | None
|
||||
effort_key: str | None
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderProfile:
|
||||
name: str
|
||||
thinking: ThinkingWire
|
||||
strip_think_tags: bool
|
||||
supports_native_schema: bool = False
|
||||
```
|
||||
|
||||
```python
|
||||
# thinking.py
|
||||
@dataclass(frozen=True)
|
||||
class ThinkingCapability:
|
||||
supported_efforts: tuple[Effort, ...]
|
||||
evidence: str
|
||||
|
||||
@property
|
||||
def can_disable(self) -> bool: ... # Effort.NONE in supported_efforts
|
||||
@property
|
||||
def cheapest_effort(self) -> Effort | None: ... # 除 NONE 外按 _ORDER 最弱的一档
|
||||
|
||||
def resolve_thinking(
|
||||
profile: ProviderProfile,
|
||||
capability: ThinkingCapability | None,
|
||||
effort: Effort | None,
|
||||
*,
|
||||
model: str,
|
||||
fallback: str = "error", # "error" | "nearest"
|
||||
warn_unregistered: bool = True,
|
||||
) -> Mapping[str, Any]: ...
|
||||
|
||||
def reconcile_thinking(
|
||||
*,
|
||||
effort: Effort | None,
|
||||
observation: ThinkingObservation,
|
||||
capability: ThinkingCapability | None,
|
||||
model: str,
|
||||
) -> str | None: ...
|
||||
```
|
||||
|
||||
```python
|
||||
# types.py 字段追加(均追加在末尾,不扰动既有位置构造)
|
||||
# SourceConfig: reasoning_effort: Effort | None = None
|
||||
# effort_fallback: str = "error"
|
||||
# ChatRequest: reasoning_effort: Effort | None = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — `Effort` 词汇与能力表重构
|
||||
|
||||
**文件**: `src/polygateway/types.py`(改)、`src/polygateway/thinking.py`(改)、`src/polygateway/__init__.py`(改)、`tests/unit/test_thinking.py`(改)、`tests/e2e/test_thinking_live.py`(改)
|
||||
|
||||
**行为**:
|
||||
1. `types.py` 新增 `Effort` 与 `_ORDER`(见上)。放 `types.py` 而非 `thinking.py`: 它是 `SourceConfig`/`ChatRequest` 的字段类型,定义在决策模块会让 `types.py` 反向 import(依赖铁律)。
|
||||
2. `ThinkingCapability` 改为 `supported_efforts` + `evidence`,加两个 `@property` 派生量。构造期校验: `supported_efforts` 非空、元素唯一、全部属 `Effort`,违反即 `ValueError`。
|
||||
3. `DEFAULT_CAPABILITIES` 按设计 §8 落库规则重写(见下表)。
|
||||
4. 迁移三处既有读点: `thinking.py` 内部读 `capability.can_disable` 改为读派生属性(行为不变);`tests/unit/test_thinking.py` 的 `ThinkingCapability(True, "实测")` 位置参数构造改为关键字构造;`tests/e2e/test_thinking_live.py` 读 `can_disable` 处确认派生属性可用。
|
||||
5. `__init__.py` 导出 `Effort`(包根导出是既有纪律: 深路径 import 正是模块重组会打断下游的原因,见 ARCH D11)。
|
||||
|
||||
**初始 `DEFAULT_CAPABILITIES`**(evidence 一律以 `2026-09-04 文档推定(来源),待经 new-api 实测` 开头):
|
||||
|
||||
| model | supported_efforts |
|
||||
|---|---|
|
||||
| `glm-5.3`, `glm-5.3-flash` | `(LOW, HIGH, MAX)` |
|
||||
| `glm-5.2` | `(NONE, HIGH, MAX)` |
|
||||
| `glm-5`, `glm-5.1`, `glm-4.6v` | `(NONE, AUTO)` |
|
||||
| `deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp` | `(NONE, HIGH, MAX)` |
|
||||
| `gpt-5.4`, `gpt-5.5` | `(NONE, LOW, MEDIUM, HIGH, XHIGH)` |
|
||||
| `claude-opus-5`, `claude-sonnet-5` | `(NONE, LOW, MEDIUM, HIGH, XHIGH, MAX)` |
|
||||
| `gemini-3.1-pro` | `(LOW, MEDIUM, HIGH)` |
|
||||
| `kimi-k3` | `(LOW, HIGH, MAX)` —— 保守登记,evidence 注明 OpenRouter 标可关但官方档位无 `none` |
|
||||
| `MiniMax-M3` | `(NONE, AUTO)` |
|
||||
| `MiniMax-M2.7`, `MiniMax-M2.5` | `(AUTO,)` |
|
||||
| `qwen-plus-latest`, `qwen3.5-flash`, `qwen3.6-plus`, `qwen3.7-max`, `qwen3.7-plus` | `(NONE, AUTO)` |
|
||||
|
||||
`claude-haiku-5`、`gemini-3-flash`、`kimi-for-coding` **不登记**(档位清单未知,走 Phase 3)。现有三条 MiniMax 条目的 evidence 原文保留并追加新形状说明——它们是实测得来的,比文档推定更硬,不得覆盖。
|
||||
|
||||
**验收**: `can_disable` 对 11 类模型的返回与上表一致;`cheapest_effort` 对 `(LOW, HIGH, MAX)` 返回 `LOW`、对 `(NONE, AUTO)` 返回 `AUTO`、对 `(AUTO,)` 返回 `AUTO`;空元组构造报 `ValueError`。
|
||||
|
||||
**测试**(先失败后通过): `tests/unit/test_thinking.py::test_capability_derives_can_disable`、`::test_cheapest_effort_skips_none`、`::test_empty_efforts_rejected`。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit/test_thinking.py tests/unit/test_package.py -v` → PASS;`conda run -n PolyGateway make lint` → PASS(含 import-linter: `Effort` 落 `types.py` 不得产生反向依赖,设计 §13 第 6 条)
|
||||
|
||||
- [ ] 提交: `refactor: make capability a tier list, since "can it be off" is one entry in it`
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — `ThinkingWire` 与 8 段 provider 表
|
||||
|
||||
**文件**: `src/polygateway/providers.py`(改)、`src/polygateway/__init__.py`(改)、`tests/unit/test_providers.py`(改)
|
||||
|
||||
**行为**:
|
||||
1. 新增 `ThinkingWire`(见关键接口)。`None` 的语义严格沿用 issue #5: `on_base is None` = **形态未知**(报错),`off is None` = 该 provider 无关闭形态,`effort_key is None` = 该 provider 无档位概念。三者语义互不重叠,docstring 必须写明。
|
||||
2. `ProviderProfile.thinking_on`/`thinking_off` 两字段替换为 `thinking: ThinkingWire`。
|
||||
3. `DEFAULT_PROFILES` 由 4 段扩到 8 段:
|
||||
|
||||
| provider | off | on_base | effort_key |
|
||||
|---|---|---|---|
|
||||
| `qwen` | `{"enable_thinking": False}` | `{"enable_thinking": True}` | `None` |
|
||||
| `deepseek` | `{"thinking": {"type": "disabled"}}` | `{"thinking": {"type": "enabled"}}` | `"reasoning_effort"` |
|
||||
| `zhipu` | `{"thinking": {"type": "disabled"}}` | `{"thinking": {"type": "enabled"}}` | `"reasoning_effort"` |
|
||||
| `moonshot` | `{"thinking": {"type": "disabled"}}` | `{"thinking": {"type": "enabled"}}` | `"reasoning_effort"` |
|
||||
| `minimax` | `{"reasoning_effort": "none"}` | `{}` | `"reasoning_effort"` |
|
||||
| `openai` | `{"reasoning_effort": "none"}` | `{}` | `"reasoning_effort"` |
|
||||
| `anthropic` | `{"reasoning_effort": "none"}` | `{}` | `"reasoning_effort"` |
|
||||
| `google` | `{"reasoning_effort": "none"}` | `{}` | `"reasoning_effort"` |
|
||||
|
||||
`__init__.py` 同步导出 `ThinkingWire`。`openai` 段的两档由 `None`(未知)改为 OpenAI 标准形态,是本任务唯一的语义变更,理由写进注释: gpt-5.x 的 `reasoning_effort` 是 OpenAI 官方字段而非厂商方言,兜底段发它不会打到不认识它的厂商;真正未知形态的 provider 仍应走 `register_provider`。
|
||||
|
||||
**验收**: `get_provider("zhipu").thinking.effort_key == "reasoning_effort"`;未注册名仍报错且错误文案列出全部 8 段;`register_provider` 仍返回新表不改共享状态。
|
||||
|
||||
**测试**(先失败后通过): `tests/unit/test_providers.py::test_all_eight_profiles_registered`、`::test_wire_none_semantics_distinct`(三种 `None` 各自的含义不混淆)。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit/test_providers.py -v` → PASS
|
||||
|
||||
- [ ] 提交: `feat: give zhipu, moonshot, anthropic and google a wire of their own`
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — `resolve_thinking` 五道关卡与 nearest 映射
|
||||
|
||||
**文件**: `src/polygateway/thinking.py`(改)、`tests/unit/test_thinking.py`(改)
|
||||
|
||||
**行为**: 按下表实现,**顺序不可调换**,每关的理由写进 docstring。
|
||||
|
||||
| Phase | 条件 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | `effort is None` | 返回 `{}` |
|
||||
| 2 | `profile.thinking.on_base is None` | `ThinkingUnsupportedError`,指路 `register_provider`/`extra_body` |
|
||||
| 3 | `capability is None` | `warn_unregistered` 为真时 warning,随后按 wire 注入,**不校验档位** |
|
||||
| 4 | `effort is NONE` 且 `not capability.can_disable` | `ThinkingUnsupportedError`,文案含 `cheapest_effort` 与 env 键名 |
|
||||
| 5 | `effort not in supported_efforts` 且 `fallback == "error"` | `ThinkingUnsupportedError`,列出该模型可选档 |
|
||||
|
||||
Phase 4 必须先于 5: `none` 只是 5 的特例,落进 5 会退化成「不支持 none,可选 low/high/max」,丢掉「这个模型根本关不掉」与可执行替代。
|
||||
|
||||
**注入形态**:
|
||||
- `effort is NONE` → `wire.off`;`wire.off is None` 时报错(该 provider 无关闭形态)。
|
||||
- `effort is AUTO` → `wire.on_base`(不附档位)。这与旧 `thinking_on` 逐字节等价。
|
||||
- 其余档 → `{**wire.on_base, wire.effort_key: effort.value}`;`effort_key is None` 时报错并说明该 provider 只有开关没有档位。
|
||||
|
||||
**nearest 映射**(`fallback == "nearest"`,人类 2026-09-04 复核确认实现): 按 `_ORDER` 在 `supported_efforts` 中取距请求档**位序最近**者,等距时**取弱侧**(省钱优先,不替下游涨价);`AUTO` 不参与距离计算,仅当它是唯一候选时才被选中;映射发生时 warning 记明「请求档 → 实际档 → 模型」。`effort is NONE` 且不可关时**不走映射**——那是 Phase 4 的领域,必须报错给出路,否则又变成静默降级。
|
||||
|
||||
**验收**: 五关各自触发与不触发;`medium` 在 `(LOW, HIGH, MAX)` 上 `nearest` 映射到 `LOW`(等距取弱);`minimal` 映射到 `LOW`;`xhigh` 映射到 `MAX`。
|
||||
|
||||
**测试**(先失败后通过): `::test_phase4_before_phase5`(请求 `none` 打到 glm-5.3,断言文案**含** `cheapest_effort` 值与 `REASONING_EFFORT`)、`::test_nearest_ties_go_cheaper`、`::test_none_never_maps`、`::test_auto_injects_on_base_only`、`::test_effort_key_none_rejects_tier`。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit/test_thinking.py -v` → PASS
|
||||
|
||||
- [ ] 提交: `feat: refuse an impossible tier with the cheapest one that model does have`
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — 源级配置入口
|
||||
|
||||
**文件**: `src/polygateway/types.py`(改)、`src/polygateway/config.py`(改)、`.env.example`(改)、`tests/unit/test_config.py`(改)
|
||||
|
||||
**行为**:
|
||||
1. `SourceConfig` 末尾追加 `reasoning_effort: Effort | None = None` 与 `effort_fallback: str = "error"`。
|
||||
2. `config.py` 的 `_SOURCE_FIELDS` 增两行: `"REASONING_EFFORT": ("reasoning_effort", "effort")`、`"EFFORT_FALLBACK": ("effort_fallback", "str")`。新增 `"effort"` 解析类型: 值必须属 `Effort` 取值域,否则报错并列出八档。
|
||||
3. `effort_fallback` 值域 `{"error", "nearest"}`,越界即报错(与 `_SELECTORS`/`_QUOTA_FULL` 同款 frozenset 校验)。
|
||||
4. **矛盾校验**(构造期): 同源同时给出 `enable_thinking` 与 `reasoning_effort` 且语义冲突时 `ValueError`。冲突定义: `enable_thinking is True` 且 `reasoning_effort is NONE`;或 `enable_thinking is False` 且 `reasoning_effort not in (None, Effort.NONE)`。二者一致(如 `False` + `none`)则放行。
|
||||
5. `.env.example` 加两键模板,注释写明八档取值、与 `ENABLE_THINKING` 的等价关系及矛盾会报错。
|
||||
|
||||
**验收**: `LLM__ZHIPU__1__REASONING_EFFORT=low` 解析为 `Effort.LOW`;写 `lowest` 报错且文案列出八档;`ENABLE_THINKING=true` + `REASONING_EFFORT=none` 构造期报错。
|
||||
|
||||
**测试**(先失败后通过): `tests/unit/test_config.py::test_effort_key_parsed`、`::test_invalid_effort_lists_vocabulary`、`::test_contradictory_thinking_flags_rejected`、`::test_consistent_flags_allowed`。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit/test_config.py -v` → PASS
|
||||
|
||||
- [ ] 提交: `feat: let a source name its reasoning tier, and say so when it contradicts itself`
|
||||
|
||||
---
|
||||
|
||||
## Task 5 — 请求级入口与优先级
|
||||
|
||||
**文件**: `src/polygateway/types.py`(改)、`src/polygateway/client.py`(改)、`tests/unit/test_client.py`(改)
|
||||
|
||||
**行为**:
|
||||
1. `ChatRequest` 末尾追加 `reasoning_effort: Effort | None = None`。
|
||||
2. `GatewayClient.chat()` 增关键字参数 `reasoning_effort: Effort | None = None`,存入 `ChatRequest`。
|
||||
3. 新增纯函数(放 `thinking.py`,与其余推理决策同处):
|
||||
|
||||
```python
|
||||
def effective_effort(
|
||||
*, request_effort: Effort | None, source_effort: Effort | None,
|
||||
enable_thinking: bool | None,
|
||||
) -> Effort | None:
|
||||
"""生效档位: 请求级 > 源级 > enable_thinking 语法糖 > None。"""
|
||||
```
|
||||
|
||||
语法糖映射: `True` → `Effort.AUTO`(注入 `on_base`,与旧行为逐字节等价,且不依赖能力表);`False` → `Effort.NONE`;`None` → 不表态。
|
||||
|
||||
**验收**: 三层优先级各自生效;请求级 `None` 不会覆盖源级已配的档;只配 `enable_thinking=True` 的存量源解析为 `AUTO` 且最终 payload 与升级前逐字节相同。
|
||||
|
||||
**测试**(先失败后通过): `::test_request_effort_wins_over_source`、`::test_none_request_does_not_clear_source`、`::test_enable_thinking_true_is_auto`、`::test_legacy_payload_byte_identical`(回归门: 存量下游行为不变)。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit/test_client.py -v` → PASS
|
||||
|
||||
- [ ] 提交: `feat: let one call ask for a different tier than its source defaults to`
|
||||
|
||||
---
|
||||
|
||||
## Task 6 — 缓存 key
|
||||
|
||||
**文件**: `src/polygateway/client.py`(改)、`src/polygateway/middleware/cache.py`(改)、`tests/unit/test_cache.py`(改)、`tests/integration/test_redis_cache.py`(改,该文件亦断言 key 形状)
|
||||
|
||||
**行为**:
|
||||
1. `_fingerprint_mark`: 源级 `reasoning_effort` **仅在非 `None` 时**追加,规则与 `enable_thinking` 完全一致——全源不表态时指纹字面量逐字不变,存量缓存不冷启动。
|
||||
2. `build_cache_key` 增关键字参数 `reasoning_effort: Effort | None = None`,**仅非 `None` 时**写入 `key_obj["reasoning_effort"]`。
|
||||
3. `CacheMW.__call__` 传 `request.reasoning_effort`。
|
||||
|
||||
**为什么两处都要**(写进注释): `model_fingerprint` 是装配期算的**集合级**指纹,覆盖不到逐次调用变化的请求级档位;不进 key 则同 messages 跑 low 与 max 互相命中,是 issue #4「5 个 seed 全命中同一响应」的逐字翻版。ARCH §7.5 记载的「集合级指纹仍可能返回另一源响应」这一既有取舍原样延续,本任务不扩大。
|
||||
|
||||
**验收**: 同 messages 不同请求级档位 → key 不同;两者皆不表态 → key 与升级前逐字相同(回归);源级档位变化 → fingerprint 变化。
|
||||
|
||||
**测试**(先失败后通过): `::test_request_tier_changes_key`、`::test_absent_tier_keeps_legacy_key`(断言具体 key 字符串不变)、`::test_source_tier_enters_fingerprint`。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit -k "cache or fingerprint" -v` → PASS
|
||||
|
||||
- [ ] 提交: `fix: keep a low-tier answer out of the cache slot a max-tier one filled`
|
||||
|
||||
---
|
||||
|
||||
## Task 7 — 遥测新增 `reasoning_effort` 列
|
||||
|
||||
**文件**: `src/polygateway/telemetry/schema.py`、`src/polygateway/ports.py`、`src/polygateway/telemetry/sqlite.py`、`src/polygateway/telemetry/postgres.py`、`src/polygateway/middleware/telemetry.py`(均改)、`tests/unit/test_telemetry.py`(改,含列数断言)、`tests/unit/test_ports.py`(改,Protocol 签名断言)
|
||||
|
||||
**行为**:
|
||||
1. `schema.py`: `COLUMNS` 末尾加 `"reasoning_effort"`(INSERT 字段 25 → 26,物理列 26 → 27);两端 DDL 追加 `reasoning_effort TEXT`(位置与 ALTER 追加一致);补列声明同步。**列数断言按物理列写**——两套口径混用是本模块最易错处(见其 docstring)。
|
||||
2. `ports.py`: `record_llm_call` 加 `reasoning_effort: str | None`(**不设默认值**,与既有约定一致: 库外无第三方实现者,少写一列会被 emitter 降级吞成 warning);docstring 的「25 字段冻结」改 26。
|
||||
3. 两个 recorder 落库新列。
|
||||
4. `middleware/telemetry.py`: `_record` 加参并传给 recorder(**唯一** `record_llm_call` 调用点,不复制参数列表);三个入口取值口径分列:
|
||||
|
||||
| 入口 | 取值 | 理由 |
|
||||
|---|---|---|
|
||||
| `emit_attempt` | `effective_effort(...)` 的结果 | 有选中源,能算出真正生效的档 |
|
||||
| `emit_cache_hit` | `request.reasoning_effort` | 缓存命中没有选中源,源级档位无从谈起 |
|
||||
| `emit_terminal_failure` | `request.reasoning_effort` | 同上(可能根本没选出源) |
|
||||
|
||||
与 `sampling` 列的现有做法同构(`emit_attempt` 合并源级,另两处只取请求级)。
|
||||
5. 值为 `Effort` 时取 `.value` 落库,`None` 落 `NULL`——与 `thinking_observation` 同一先例(`StrEnum` 是 `str` 子类,asyncpg 对子类编码不保证接受,遥测写失败只降级 warning,PG 那一路会静默少列)。
|
||||
|
||||
**验收**: 两端建表列数断言更新且通过;三个入口各自落值正确;不表态时为 `NULL`;`telemetry_schema_sql` 打印的 SQL 与库实际执行的 DDL 同源。
|
||||
|
||||
**测试**(先失败后通过): 既有遥测列数断言用例更新;`::test_effort_column_records_effective_tier`、`::test_cache_hit_records_request_tier_only`、`::test_absent_tier_is_null`。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit tests/integration -k telemetry -v` → PASS
|
||||
|
||||
- [ ] 提交: `feat: record which tier a call actually ran at`
|
||||
|
||||
---
|
||||
|
||||
## Task 8 — transport 接线与对账
|
||||
|
||||
**文件**: `src/polygateway/transports/openai_compat.py`(改)、`src/polygateway/thinking.py`(改)、`tests/unit/test_openai_compat.py`(改)
|
||||
|
||||
**行为**:
|
||||
1. `_build_payload`: 用 `effective_effort(...)` 求生效档位后调 `resolve_thinking(..., fallback=source.effort_fallback)`。注入结果仍**先于** `source.extra_body` 与 `overlay`(顺序即优先级,issue #4 决策 A,两行不可调换)。
|
||||
2. `_warn_on_thinking_mismatch` 的节流键由 `(source.name, source.model, source.enable_thinking)` 改为 `(source.name, source.model, effective_effort)`——同一模型的 low 与 max 是两个独立的矛盾,共用一个键会让第二个永久静音。
|
||||
3. `reconcile_thinking` 签名的 `enable_thinking: bool | None` 改为 `effort: Effort | None`,判据: `effort is NONE` 对应原「要求关闭」分支,`effort` 为其余档对应原「要求开启」分支,`None` 仍返回 `None`。**不新增**「档位高低 vs `reasoning_tokens` 多少」的对账(设计 §4.3: 无可判定的函数关系,拿它报警必然是噪声)。
|
||||
4. `ThinkingUnsupportedError` 的捕获与翻译路径不变(→ `RequestRejectedError`,不重试不换源不计熔断)。
|
||||
|
||||
**验收**: 档位不支持时抛 `RequestRejectedError` 且不触发重试与熔断计数;同源同模型不同档各喊一次告警;`reconcile` 三类文案与既有逐字一致(除方向描述由 bool 改档位)。
|
||||
|
||||
**测试**(先失败后通过): `::test_unsupported_tier_is_request_rejected`、`::test_no_retry_on_tier_error`、`::test_throttle_key_separates_tiers`、`::test_reconcile_none_vs_observed`。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/unit -k "transport or openai_compat" -v` → PASS
|
||||
|
||||
- [ ] 提交: `feat: wire the tier through the transport and keep each tier's warning distinct`
|
||||
|
||||
---
|
||||
|
||||
## Task 9 — 全套件、文档与 wiki
|
||||
|
||||
**文件**: `CHANGELOG.md`、`.env.example`(复核)、Gitea Wiki(按 `research-wiki/docs-convention.md` §2)、`src/polygateway/__init__.py`(版本号)、`pyproject.toml`(版本号)
|
||||
|
||||
**行为**:
|
||||
1. `make lint` + `make test` 全绿;`make format`。
|
||||
2. CHANGELOG 加「未发布」段: 破坏性变更(`ThinkingCapability` 构造签名)、新增(八档 `Effort`、两个 env 键、遥测新列、四个 provider 段)、行为变更(`openai` 段两档由未知改为 OpenAI 标准形态)。
|
||||
3. 按 docs-convention §2 同步 wiki(公共行为变更必须同步,版本 bump 不得裸发)。
|
||||
4. 版本号 `1.4.0`(minor: 公共 API 破坏性变更),两处一致。**本任务只 bump 不发布**——发布走 CLAUDE.md §4.4.1 全清单。
|
||||
|
||||
**验收**: `make ci` 通过;CHANGELOG 与 wiki 均含破坏性变更条目。
|
||||
|
||||
**验证**: `conda run -n PolyGateway make ci` → PASS
|
||||
|
||||
- [ ] 提交: `docs: cut 1.4.0 notes for the tier work`
|
||||
|
||||
---
|
||||
|
||||
## Task 10 — e2e 实测校正初始能力表(标 `slow`)
|
||||
|
||||
**文件**: `tests/e2e/test_thinking_live.py`(改)
|
||||
|
||||
**行为**: 对 §Task 1 表中每个已登记模型,经 new-api 实测其 `supported_efforts`,方法论沿用 issue #20: 固定短提示词,逐档 N≥5,判据取 `usage.completion_tokens_details.reasoning_tokens`;对声明不可关的模型额外验证「请求 `none` 是否真被拒或真未关」。测试标 `slow`(成败取决于外部服务当下状态,默认不进日常套件)。实测结论逐条替换 `evidence` 中的「文档推定」。
|
||||
|
||||
**为什么必须单列一个任务**: 人类 2026-09-04 定「能力表数据统一自己经 new-api 实测」;Task 1 落的是文档推定值,不实测则整张表都是假设。
|
||||
|
||||
**验收**: 每个已登记模型有一条实测记录;与文档推定不符者更新 `supported_efforts` 并在 evidence 记明分歧(尤其 `kimi-k3` 的保守登记、`gemini-3.1-pro` 的默认档两源打架)。
|
||||
|
||||
**验证**: `conda run -n PolyGateway pytest tests/e2e/test_thinking_live.py -m slow -v` → PASS(约 20-40 分钟,取决于网关)
|
||||
|
||||
- [ ] 提交: `test: replace the guessed tier table with what the gateway actually does`
|
||||
|
||||
---
|
||||
|
||||
## 执行顺序与依赖
|
||||
|
||||
```
|
||||
T1(词汇+能力表) ──┬─→ T3(五关) ──→ T8(transport)
|
||||
T2(wire) ─────────┘ ↑
|
||||
T4(源级) ──┬─→ T5(请求级) ──┬────────┘
|
||||
│ ├─→ T6(缓存 key)
|
||||
│ └─→ T7(遥测)
|
||||
↓
|
||||
T9(文档) → T10(实测)
|
||||
```
|
||||
|
||||
T1/T2 可并行;T3 依赖两者;T5 依赖 T4(语法糖等价关系);T6/T7 依赖 T5(请求级字段);T8 依赖 T3+T5;T9 在功能任务全绿后;T10 最后且独立(标 `slow`)。
|
||||
|
||||
执行方式: 10 个任务耦合度中等(共享 `Effort`/`ThinkingCapability`/`ThinkingWire` 三个类型),**直接按计划实现**,不派 `subagent-driven-development`——跨任务共享类型多,独立上下文的 subagent 容易在签名上分叉。
|
||||
Reference in New Issue
Block a user