feat: refuse an impossible tier with the cheapest one that model does have

resolve_thinking now takes an Effort instead of a tri-state bool, and the
four gates become five. The new one sits ahead of the generic tier check
on purpose: asking for `none` on GLM-5.3 used to fall through to "none is
not supported, pick low/high/max", which loses both the fact that the
model cannot stop reasoning and the one tier the caller could switch to
right now. Without that alternative, downstream goes looking for
extra_body — which is how issue #20 happened in the first place.

The return type is a ThinkingResolution rather than the payload alone.
Under fallback="nearest" the tier that goes out is not the tier that was
asked for, and telemetry has to record the one that ran, or task 10 files
a call under a tier it never used. Ties in that mapping go to the weaker
side: a silent medium -> max is a multiple of the bill, and the library
does not raise a caller's price on its own.

Two readings the design left implicit, both settled the way its own
compatibility promise requires:

- `auto` is exempt from the tier list. It means "on, no tier named",
  which in the body is the absence of the effort key, not a value of it.
  Checking it against the list would break every existing source that
  sets ENABLE_THINKING=true against deepseek-v4 or glm-5.3.
- `none` is never a mapping target. Turning "think less" into "do not
  think" reverses the decision instead of cheapening it; a switch-only
  model maps to `auto` and a model that only has `none` still errors.

Both call sites convert enable_thinking in place for now; task 5 folds
that into effective_effort along with the source- and call-level tiers.
This commit is contained in:
2026-09-05 01:36:30 -04:00
parent 84230673b9
commit a1c4273a8b
6 changed files with 473 additions and 65 deletions
+13 -3
View File
@@ -34,7 +34,12 @@ from polygateway.thinking import (
resolve_thinking,
)
from polygateway.transports._http_errors import compose_message, summarize_body
from polygateway.types import EmbeddingTransportResult, SourceConfig, TransportResult
from polygateway.types import (
Effort,
EmbeddingTransportResult,
SourceConfig,
TransportResult,
)
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Callable, Mapping
@@ -355,14 +360,19 @@ class OpenAICompatTransport:
capability = get_capability(source.model, table=self._capabilities)
first_time = source.model not in self._warned_models
self._warned_models.add(source.model)
# `enable_thinking` 的档位语法糖(True → auto,False → none,None 不表态);
# 就地转换是过渡形态,T5 起由 thinking.effective_effort() 统一收口并接上
# 源级/请求级档位(设计 §4.2)
enabled = source.enable_thinking
effort = None if enabled is None else (Effort.AUTO if enabled else Effort.NONE)
payload.update(
resolve_thinking(
profile,
capability,
source.enable_thinking,
effort,
model=source.model,
warn_unregistered=first_time,
)
).payload
)
# 顺序即优先级(issue #4 设计决策 A): 配置级 extra_body 在前,调用级
# overlay(含结构化注入)在后覆盖之。两行不可调换