fix: read a tier the way every config path actually spells it

Both public assembly paths took the tier on trust: a bare "none" from
JSON or a hand-built SourceConfig stayed a str, and `is Effort.NONE`
then read it as a contradiction and crashed on `.value` while wording
the error -- the caller got an AttributeError where a ValueError was
promised, and on the request side that unclassified exception walked
straight through the transport's ThinkingUnsupportedError catch and the
retry classifier.

Normalize at the two entrances instead, matching what the .env path has
always done, and let EFFORT_FALLBACK be spelled with the same freedom as
its neighbour.
This commit is contained in:
2026-09-05 04:07:06 -04:00
parent 701a8a6841
commit 1a35d515d9
6 changed files with 173 additions and 26 deletions
+5 -21
View File
@@ -22,10 +22,10 @@ from loguru import logger
from polygateway.types import (
BackpressurePolicy,
BreakerConfig,
Effort,
GlobalLimits,
RetryPolicy,
SourceConfig,
coerce_effort,
)
if TYPE_CHECKING:
@@ -47,6 +47,7 @@ _SOURCE_FIELDS: dict[str, tuple[str, str]] = {
# 档位两键(issue #20);值域校验分工: 档位在此(解析即校验,报错点得出 env 键名),
# fallback 交给 SourceConfig 构造期(那道同时覆盖构造函数注入与 dataclasses.replace)
"REASONING_EFFORT": ("reasoning_effort", "effort"),
# 归一化(strip+lower)在 SourceConfig 构造期,与值域校验同处一点,故这里是裸 "str"
"EFFORT_FALLBACK": ("effort_fallback", "str"),
"MISSING_DONE": ("missing_done", "str"),
"TRUST_ENV": ("trust_env", "bool"),
@@ -99,7 +100,9 @@ def _cast(raw: str, kind: str, key: str) -> object:
return False
raise ValueError(f"非法布尔值: {raw!r}")
if kind == "effort":
return _to_effort(raw)
# 归一化只有一份实现(`types.coerce_effort`),env 路与两条装配路同口径;
# origin 传空串是因为 env 键名由下面统一的"配置 X 解析失败"补上
return coerce_effort(raw, origin="")
if kind == "json":
# JSONDecodeError 是 ValueError 子类,复用下方的统一包装
parsed = json.loads(raw)
@@ -111,25 +114,6 @@ def _cast(raw: str, kind: str, key: str) -> object:
raise ValueError(f"配置 {key} 解析失败: {exc}") from exc
def _to_effort(raw: str) -> Effort:
"""把 env 字符串解成 `Effort`;越界时报错并**列出全部八档**。
列全八档不是啰嗦: 档位词汇是封闭的,而下游会照着别处的习惯写
(`lowest`/`off`/`disabled` 都出现过),只说"非法值"等于让人去翻源码。
`strip().lower()` 与 `bool` 分支同一先例: `.env` 里的行尾空格与大写写法
是常态,而档位取值本身没有大小写语义(`Effort` 的值全小写)。
"""
try:
return Effort(raw.strip().lower())
except ValueError:
# 不 `from exc`: 枚举原生的 "'lowest' is not a valid Effort" 只是同一
# 件事的英文复述,链上去反而把可操作的那句挤到后面
raise ValueError(
f"非法推理档位 {raw!r};允许: {', '.join(e.value for e in Effort)}"
) from None
def _first(env: Mapping[str, str], *keys: str) -> tuple[str, str] | None:
for key in keys:
raw = env.get(key)