fix: judge the phase 2 shape by the tier that was asked for
A provider that registered a disable form but no enable form was told its shape was unknown and pointed at register_provider -- work it had already done -- for a request that only ever needed the disable form. The old bool code took the slot by direction; the tiered rewrite lost that. Take the relevant field again, and keep "shape unknown" for the case where both halves are missing, so the "cannot disable" wording still owns the half-missing case.
This commit is contained in:
@@ -128,7 +128,7 @@ cherry 有两层我们**明确不做**:
|
|||||||
| Phase | 条件 | 结果 |
|
| Phase | 条件 | 结果 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | 生效档位为 `None`(调用方不表态) | 返回 `{}`,不注入 |
|
| 1 | 生效档位为 `None`(调用方不表态) | 返回 `{}`,不注入 |
|
||||||
| 2 | **该请求档所需的**形态未知(请求 `none` 看 `wire.off`,其余档看 `wire.on_base`) | `ThinkingUnsupportedError`,指路 `register_provider`/`extra_body` |
|
| 2 | **该请求档所需的**形态未知(请求 `none` 看 `wire.off`,其余档看 `wire.on_base`;`none` 方向须 `off` 与 `on_base` **皆为 `None`** 才算「整体形态未知」——单 `off is None` 是「该 provider 关不掉」,归 `_inject` 说清缺的是哪半边,2026-09-05 实现时补正) | `ThinkingUnsupportedError`,指路 `register_provider`/`extra_body` |
|
||||||
| 3 | 能力未登记 | warning 后按 wire 尽力注入,**不校验档位** |
|
| 3 | 能力未登记 | warning 后按 wire 尽力注入,**不校验档位** |
|
||||||
| 4 | 请求 `none` 而该模型无 `none` 档 | `ThinkingUnsupportedError`,**给出 `cheapest_effort` 作为替代** |
|
| 4 | 请求 `none` 而该模型无 `none` 档 | `ThinkingUnsupportedError`,**给出 `cheapest_effort` 作为替代** |
|
||||||
| 5 | 其余档位不在 `supported_efforts` 且未开映射 | `ThinkingUnsupportedError`,列出该模型可选档 |
|
| 5 | 其余档位不在 `supported_efforts` 且未开映射 | `ThinkingUnsupportedError`,列出该模型可选档 |
|
||||||
|
|||||||
+29
-11
@@ -14,7 +14,7 @@ from typing import Any
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from polygateway.providers import ProviderProfile
|
from polygateway.providers import ProviderProfile, ThinkingWire
|
||||||
from polygateway.types import EFFORT_ORDER, Effort, ThinkingObservation
|
from polygateway.types import EFFORT_ORDER, Effort, ThinkingObservation
|
||||||
|
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ def resolve_thinking(
|
|||||||
return ThinkingResolution({}, None)
|
return ThinkingResolution({}, None)
|
||||||
wire = profile.thinking
|
wire = profile.thinking
|
||||||
# Phase 2: 形态未知 —— 给了档位却不知道怎么发,静默放行就是欺骗调用方
|
# Phase 2: 形态未知 —— 给了档位却不知道怎么发,静默放行就是欺骗调用方
|
||||||
if wire.on_base is None:
|
if _wire_unknown_for(wire, effort):
|
||||||
raise ThinkingUnsupportedError(
|
raise ThinkingUnsupportedError(
|
||||||
f"provider {profile.name!r} 的推理形态未知(模型 {model!r},请求档位 "
|
f"provider {profile.name!r} 的推理形态未知(模型 {model!r},请求档位 "
|
||||||
f"{effort.value!r}): 本库不知道该 provider 如何表达推理。请用 "
|
f"{effort.value!r}): 本库不知道该 provider 如何表达推理。请用 "
|
||||||
@@ -399,7 +399,7 @@ def resolve_thinking(
|
|||||||
)
|
)
|
||||||
# Phase 3: 能力未登记 —— 新模型上线不该被库挡住,但也不该假装成功
|
# Phase 3: 能力未登记 —— 新模型上线不该被库挡住,但也不该假装成功
|
||||||
if capability is None:
|
if capability is None:
|
||||||
payload = _inject(profile, wire.on_base, effort, model=model)
|
payload = _inject(profile, effort, model=model)
|
||||||
if warn_unregistered:
|
if warn_unregistered:
|
||||||
_warn_unregistered(model, profile, effort, payload)
|
_warn_unregistered(model, profile, effort, payload)
|
||||||
return ThinkingResolution(payload, effort)
|
return ThinkingResolution(payload, effort)
|
||||||
@@ -409,7 +409,24 @@ def resolve_thinking(
|
|||||||
raise ThinkingUnsupportedError(_cannot_disable(model, capability))
|
raise ThinkingUnsupportedError(_cannot_disable(model, capability))
|
||||||
# Phase 5: 档位打空 —— 报错或按 fallback 映射(auto 例外,见 docstring)
|
# Phase 5: 档位打空 —— 报错或按 fallback 映射(auto 例外,见 docstring)
|
||||||
applied = _settle_tier(effort, capability, model=model, fallback=fallback)
|
applied = _settle_tier(effort, capability, model=model, fallback=fallback)
|
||||||
return ThinkingResolution(_inject(profile, wire.on_base, applied, model=model), applied)
|
return ThinkingResolution(_inject(profile, applied, model=model), applied)
|
||||||
|
|
||||||
|
|
||||||
|
def _wire_unknown_for(wire: ThinkingWire, effort: Effort) -> bool:
|
||||||
|
"""Phase 2 的判据: **按请求档取相关字段**,不是一律看 `on_base`。
|
||||||
|
|
||||||
|
旧版 `slot = thinking_on if enable_thinking else thinking_off` 即如此。只看
|
||||||
|
`on_base` 会让"关闭形态已知、开启形态未知"的自定义 provider 在请求 `none` 时
|
||||||
|
被误拒,且指向它已经做过的 `register_provider`(设计 §2 处置表第 2 条)。
|
||||||
|
|
||||||
|
请求 `none` 时判据是**两者皆 None**,而不是单看 `off`: `ThinkingWire` 的三个
|
||||||
|
`None` 语义互不重叠——`off is None` 而 `on_base` 已知是"该 provider 关不掉"
|
||||||
|
(由 `_inject` 说清是缺了哪半边),只有两者皆 None 才是"整个形态未知",此时
|
||||||
|
指路 `register_provider` 才是对的方向。
|
||||||
|
"""
|
||||||
|
if effort is not Effort.NONE:
|
||||||
|
return wire.on_base is None
|
||||||
|
return wire.off is None and wire.on_base is None
|
||||||
|
|
||||||
|
|
||||||
def _settle_tier(
|
def _settle_tier(
|
||||||
@@ -437,13 +454,12 @@ def _settle_tier(
|
|||||||
return mapped
|
return mapped
|
||||||
|
|
||||||
|
|
||||||
def _inject(
|
def _inject(profile: ProviderProfile, effort: Effort, *, model: str) -> Mapping[str, Any]:
|
||||||
profile: ProviderProfile, on_base: Mapping[str, Any], effort: Effort, *, model: str
|
|
||||||
) -> Mapping[str, Any]:
|
|
||||||
"""按 wire 把档位写成请求体片段;wire 表达不了这一档时报错。
|
"""按 wire 把档位写成请求体片段;wire 表达不了这一档时报错。
|
||||||
|
|
||||||
`on_base` 由调用方传入而非在此重读,是为了让"Phase 2 已保证它不是 None"这条
|
自己重读 `wire` 而不由调用方传 `on_base`: Phase 2 的判据按请求档取相关字段
|
||||||
前提在签名上看得见。
|
(`none` 看 `off`,其余档看 `on_base`)之后,"on_base 一定不是 None"这条前提
|
||||||
|
只对非 `none` 档成立,写进签名反而是句假话。
|
||||||
|
|
||||||
三种 `None` 的语义在此**各自兑现**(ThinkingWire 的 docstring 定义了它们):
|
三种 `None` 的语义在此**各自兑现**(ThinkingWire 的 docstring 定义了它们):
|
||||||
`off is None` = 该 provider 关不掉,`effort_key is None` = 它只有开关没有档位。
|
`off is None` = 该 provider 关不掉,`effort_key is None` = 它只有开关没有档位。
|
||||||
@@ -459,16 +475,18 @@ def _inject(
|
|||||||
f"需要不推理请换用支持关闭的 provider 或模型"
|
f"需要不推理请换用支持关闭的 provider 或模型"
|
||||||
)
|
)
|
||||||
return wire.off
|
return wire.off
|
||||||
|
# 非 none 档的开启形态由 Phase 2 保证已知(内部不变量,不承担生产校验)
|
||||||
|
assert wire.on_base is not None
|
||||||
if effort is Effort.AUTO:
|
if effort is Effort.AUTO:
|
||||||
# auto = 开启但不指定强度: 逐字节等于升级前的 `thinking_on`
|
# auto = 开启但不指定强度: 逐字节等于升级前的 `thinking_on`
|
||||||
return on_base
|
return wire.on_base
|
||||||
if wire.effort_key is None:
|
if wire.effort_key is None:
|
||||||
raise ThinkingUnsupportedError(
|
raise ThinkingUnsupportedError(
|
||||||
f"provider {profile.name!r} 只有推理开关、没有档位键(模型 {model!r}),"
|
f"provider {profile.name!r} 只有推理开关、没有档位键(模型 {model!r}),"
|
||||||
f"表达不了 reasoning_effort={effort.value!r}: 请改用 auto/none 两档,"
|
f"表达不了 reasoning_effort={effort.value!r}: 请改用 auto/none 两档,"
|
||||||
f"或用 register_provider 给该 provider 注册 effort_key"
|
f"或用 register_provider 给该 provider 注册 effort_key"
|
||||||
)
|
)
|
||||||
return {**on_base, wire.effort_key: effort.value}
|
return {**wire.on_base, wire.effort_key: effort.value}
|
||||||
|
|
||||||
|
|
||||||
def _cannot_disable(model: str, capability: ThinkingCapability) -> str:
|
def _cannot_disable(model: str, capability: ThinkingCapability) -> str:
|
||||||
|
|||||||
@@ -177,6 +177,41 @@ class TestResolveThinking:
|
|||||||
assert "kimi-k3" in msg
|
assert "kimi-k3" in msg
|
||||||
assert effort.value in msg
|
assert effort.value in msg
|
||||||
|
|
||||||
|
def test_phase2_reads_the_form_the_asked_for_tier_needs(self):
|
||||||
|
"""请求 `none` 只需要**关闭**形态: 开启形态未知与这次请求无关。
|
||||||
|
|
||||||
|
旧版 `slot = thinking_on if enable_thinking else thinking_off` 即按请求方向
|
||||||
|
取字段;档位化后一度写成"只看 `on_base`",于是一个已注册了关闭形态的自定义
|
||||||
|
provider 在请求 `none` 时被误拒,还被指向它已经做过的 `register_provider`
|
||||||
|
——指错方向比不指更糟(设计 §2 处置表第 2 条,2026-09-05 独立验证查出)。
|
||||||
|
"""
|
||||||
|
profile = ProviderProfile(
|
||||||
|
name="off_only",
|
||||||
|
thinking=ThinkingWire(
|
||||||
|
off={"thinking": {"type": "disabled"}}, on_base=None, effort_key=None
|
||||||
|
),
|
||||||
|
strip_think_tags=False,
|
||||||
|
)
|
||||||
|
cap = ThinkingCapability((Effort.NONE, Effort.AUTO), "构造: 关得掉,开启形态却未登记")
|
||||||
|
got = resolve_thinking(profile, cap, Effort.NONE, model="x-1")
|
||||||
|
assert got.payload == {"thinking": {"type": "disabled"}}
|
||||||
|
assert got.applied_effort is Effort.NONE
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("effort", [Effort.AUTO, Effort.HIGH])
|
||||||
|
def test_phase2_still_fires_when_the_on_form_is_the_missing_half(self, effort):
|
||||||
|
"""反方向不得被一并放过: 要开推理而开启形态未知,仍须报错并指路注册。"""
|
||||||
|
profile = ProviderProfile(
|
||||||
|
name="off_only",
|
||||||
|
thinking=ThinkingWire(
|
||||||
|
off={"thinking": {"type": "disabled"}}, on_base=None, effort_key=None
|
||||||
|
),
|
||||||
|
strip_think_tags=False,
|
||||||
|
)
|
||||||
|
cap = ThinkingCapability((Effort.NONE, Effort.AUTO, Effort.HIGH), "构造")
|
||||||
|
with pytest.raises(ThinkingUnsupportedError, match="register_provider") as exc:
|
||||||
|
resolve_thinking(profile, cap, effort, model="x-1")
|
||||||
|
assert effort.value in str(exc.value)
|
||||||
|
|
||||||
def test_phase2_beats_the_capability_checks(self):
|
def test_phase2_beats_the_capability_checks(self):
|
||||||
"""形态未知时无从注入,能力如何无关紧要——Phase 2 必须先于 4/5。"""
|
"""形态未知时无从注入,能力如何无关紧要——Phase 2 必须先于 4/5。"""
|
||||||
cap = ThinkingCapability((Effort.AUTO,), "构造")
|
cap = ThinkingCapability((Effort.AUTO,), "构造")
|
||||||
|
|||||||
Reference in New Issue
Block a user