fix: reject conflicting raw reasoning overrides before sending

This commit is contained in:
2026-09-09 01:26:34 -04:00
parent 1ee74c35a8
commit 8e61a66342
4 changed files with 213 additions and 110 deletions
+109 -57
View File
@@ -623,8 +623,8 @@ class TestThinkingReconciliation:
try:
await _complete(transport, self._minimax(False))
await _complete(transport, self._minimax(False))
await _complete(transport, self._minimax(True))
await _complete(transport, self._minimax(True))
await _complete(transport, self._minimax(True), reasoning_effort=Effort.MEDIUM)
await _complete(transport, self._minimax(True), reasoning_effort=Effort.MEDIUM)
finally:
logger.remove(sink_id)
hits = [m for m in messages if "MiniMax-M3" in m]
@@ -705,75 +705,44 @@ class TestNonStreamFastPath:
class TestRequestShaping:
@pytest.mark.parametrize(
("enable_thinking", "expected"),
[(True, {"enable_thinking": True}), (False, {"enable_thinking": False}), (None, {})],
)
async def test_thinking_tri_state_injection(self, enable_thinking, expected):
@pytest.mark.parametrize("tier", [Effort.MEDIUM, Effort.NONE])
async def test_minimax_explicit_tier_is_sent(self, tier):
seen = {}
def handler(request):
seen.update(json.loads(request.content))
return _sse_stream(_chunk(content="x"), _chunk(usage=_USAGE))
await _complete(_transport_for(handler), _source(enable_thinking=enable_thinking))
assert {k: seen[k] for k in expected} == expected
if enable_thinking is None:
transport = _transport_for(handler)
try:
result = await _complete(
transport, _source(provider="minimax", model="MiniMax-M3"), reasoning_effort=tier
)
assert seen["reasoning_effort"] == tier.value
assert result.applied_effort is tier
assert "enable_thinking" not in seen
assert seen["stream_options"] == {"include_usage": True}
finally:
await transport.aclose()
@pytest.mark.parametrize(
("enable_thinking", "expected"),
# 本条断言反复过一次,记下原委以免第三次改回去:
# T2(2026-09-04)按"MiniMax 开启档本就无需参数"的**推定**把 medium 改成不注入;
# T10(2026-09-05)真实网关实测推翻该推定——M3 不发任何推理参数时 5/5 轮不推理,
# 故 medium 回归(issue #21 的权宜之计,正解是让 auto 受能力表约束)
[(True, "medium"), (False, "none")],
)
async def test_minimax_injects_reasoning_effort(self, enable_thinking, expected):
"""issue #5: MiniMax 认的是 reasoning_effort,不是 enable_thinking。"""
async def test_raw_only_keeps_source_then_request_priority(self):
seen = {}
def handler(request):
seen.update(json.loads(request.content))
return _sse_stream(_chunk(content="x"), _chunk(usage=_USAGE))
source = _source(
name="mm", provider="minimax", model="MiniMax-M3", enable_thinking=enable_thinking
)
await _complete(_transport_for(handler), source)
if expected is None:
assert "reasoning_effort" not in seen
else:
assert seen["reasoning_effort"] == expected
assert "enable_thinking" not in seen # 旧形态实测被静默丢弃,不再下发
async def test_extra_body_overrides_the_profile_slot(self):
"""注入顺序即优先级: profile → extra_body → overlay,两行不可调换。
固定用 **zhipu + glm-5.3 + 源级 low** 这组: 判据必须落在一个 profile
**真的写了值**的键上,两边写同一个键才谈得上谁覆盖谁。不挑 minimax 是因为
它的 `on_base` 只写 `reasoning_effort` 一个键(issue #21 的权宜之计),
覆盖发生后看不见"profile 独有的那半边仍在",判据少一半。
"""
seen = {}
def handler(request):
seen.update(json.loads(request.content))
return _sse_stream(_chunk(content="x"), _chunk(usage=_USAGE))
source = _source(
name="zp",
provider="zhipu",
model="glm-5.3",
reasoning_effort="low",
extra_body={"reasoning_effort": "high"},
)
await _complete(_transport_for(handler), source)
# profile 注入的是 low,extra_body 后写故发出去的是 high;顺序一调换就变 low,
# 即下游写在 extra_body 里的覆盖被库悄悄顶掉(issue #20 的成因形态)
assert seen["reasoning_effort"] == "high"
assert seen["thinking"] == {"type": "enabled"} # profile 独有的那半边仍在
transport = _transport_for(handler)
try:
result = await _complete(
transport,
_source(extra_body={"reasoning_effort": "low", "temperature": 0}),
overlay={"reasoning_effort": "high", "temperature": 1},
)
assert seen["reasoning_effort"] == "high"
assert seen["temperature"] == 1
assert result.applied_effort is None
finally:
await transport.aclose()
async def test_model_that_cannot_disable_is_rejected_not_silently_ignored(self):
"""M2.x 关不掉推理: 必须是四分类之一的 RequestRejected,不是裸 ValueError。
@@ -1056,3 +1025,86 @@ class TestLifecycle:
await _complete(transport, _source())
await transport.aclose()
await transport.aclose()
class TestManagedReasoningOwnership:
"""通过真实 transport 验证拒绝发生在 HTTP 之前。"""
@pytest.mark.parametrize(
"raw",
[
{"reasoning_effort": "high"},
{"enable_thinking": True},
{"thinking": {}},
{"thinking_budget": 100},
{"reasoning": {}},
{"thinkingConfig": {}},
{"output_config": {"effort": "low"}},
],
)
@pytest.mark.parametrize("layer", ["source", "request", "shadowed"])
async def test_raw_control_is_rejected_before_http(self, raw, layer):
sent = []
def handler(request):
sent.append(request)
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
source = _source(
provider="openai",
reasoning_effort=Effort.HIGH,
extra_body=raw if layer != "request" else {},
)
overlay = raw if layer != "source" else {}
transport = _transport_for(handler)
try:
with pytest.raises(RequestRejectedError):
await _complete(transport, source, overlay=overlay)
assert sent == []
finally:
await transport.aclose()
class TestDefaultClientFactory:
"""直接检查生产 factory,不用取证测试的另一套 factory 代替。"""
@pytest.mark.parametrize("key", ["fake-a", "fake-b"])
async def test_authorization_uses_source_api_key(self, key):
from polygateway.transports.openai_compat import _default_client_factory
client = _default_client_factory(_source(api_key=key))
try:
assert client.headers["Authorization"] == f"Bearer {key}"
assert (
client.build_request("POST", "https://gw.example/v1/chat/completions").headers[
"Authorization"
]
== f"Bearer {key}"
)
finally:
await client.aclose()
@pytest.mark.parametrize("timeout", [17.0, 53.0])
async def test_timeout_uses_source_timeout_for_all_phases(self, timeout):
from polygateway.transports.openai_compat import _default_client_factory
client = _default_client_factory(_source(timeout_s=timeout))
try:
assert [
client.timeout.connect,
client.timeout.read,
client.timeout.write,
client.timeout.pool,
] == [timeout] * 4
finally:
await client.aclose()
@pytest.mark.parametrize("trust_env", [True, False])
async def test_trust_env_uses_source_setting(self, trust_env):
from polygateway.transports.openai_compat import _default_client_factory
client = _default_client_factory(_source(trust_env=trust_env))
try:
assert client.trust_env is trust_env
finally:
await client.aclose()