feat: carry the per-call tier down to the transport that must send it
The Transport port took the request apart into five arguments, so a tier placed on ChatRequest could never reach _build_payload: the field was set, read by nobody, and silently ignored - the exact shape of failure that sent downstream to extra_body in the first place. complete() now takes reasoning_effort with no default, matching the TelemetryRecorder convention: a default would turn a missing hand-off into a silent 'no opinion'. All four fakes move with it, since @runtime_checkable checks method names and not signatures. EmbeddingTransport and OcrTransport are deliberately left alone - they have no reasoning semantics - and a test now holds that line. _build_payload drops its inline sugar conversion for effective_effort(), so the guard and the hot path share one judgement, and passes the source's effort_fallback for the same reason.
This commit is contained in:
@@ -24,6 +24,7 @@ from polygateway.types import (
|
||||
BackpressurePolicy,
|
||||
BreakerConfig,
|
||||
ChatRequest,
|
||||
Effort,
|
||||
GlobalLimits,
|
||||
RetryPolicy,
|
||||
SourceConfig,
|
||||
@@ -63,14 +64,21 @@ def _ok(content="ok"):
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
"""按脚本逐次返回结果或抛异常;记录每次 (source_name, call_id)。"""
|
||||
"""按脚本逐次返回结果或抛异常;记录每次 (source_name, call_id) 与收到的档位。
|
||||
|
||||
`reasoning_effort` 刻意**不给默认值**,与 `Transport` 协议保持逐字一致:
|
||||
`@runtime_checkable` 只查方法名不查签名,fake 上多一个默认值就会把"中间件漏传"
|
||||
这类缺口伪装成"调用方没表态",而报错现场离根因很远。
|
||||
"""
|
||||
|
||||
def __init__(self, script):
|
||||
self.script = list(script)
|
||||
self.calls = []
|
||||
self.efforts = []
|
||||
|
||||
async def complete(self, *, messages, source, stream, overlay, call_id):
|
||||
async def complete(self, *, messages, source, stream, overlay, call_id, reasoning_effort):
|
||||
self.calls.append((source.name, call_id))
|
||||
self.efforts.append(reasoning_effort)
|
||||
action = self.script.pop(0)
|
||||
if isinstance(action, Exception):
|
||||
raise action
|
||||
@@ -135,6 +143,37 @@ def _harness(
|
||||
_REQ = ChatRequest(messages=[{"role": "user", "content": "hi"}])
|
||||
|
||||
|
||||
class TestRequestTierReachesTransport:
|
||||
"""请求级档位必须一路穿过洋葱到达 transport(Task 5b)。
|
||||
|
||||
`ChatRequest` 上填了字段而中间件不搬运,是"看起来配了、实际没发出去"的静默
|
||||
失效——正是 issue #20 里下游改用 extra_body 绕过治理的成因。
|
||||
"""
|
||||
|
||||
async def test_request_tier_reaches_transport(self):
|
||||
mw, _, _, transport, *_ = _harness([_src("a")], [_ok()])
|
||||
await mw(
|
||||
ChatRequest(messages=[{"role": "user", "content": "hi"}], reasoning_effort=Effort.HIGH)
|
||||
)
|
||||
assert transport.efforts == [Effort.HIGH]
|
||||
|
||||
async def test_absent_tier_is_carried_as_none(self):
|
||||
"""不表态也要显式传下去: 漏传与"传了 None"在协议上必须区分不开才安全。"""
|
||||
mw, _, _, transport, *_ = _harness([_src("a")], [_ok()])
|
||||
await mw(_REQ)
|
||||
assert transport.efforts == [None]
|
||||
|
||||
async def test_tier_is_carried_on_every_retry_attempt(self):
|
||||
"""换源重试时档位不得在第二次尝试上丢失。"""
|
||||
mw, _, _, transport, *_ = _harness(
|
||||
[_src("a"), _src("b")], [TransientError("boom", source_name="a"), _ok()]
|
||||
)
|
||||
await mw(
|
||||
ChatRequest(messages=[{"role": "user", "content": "hi"}], reasoning_effort=Effort.LOW)
|
||||
)
|
||||
assert transport.efforts == [Effort.LOW, Effort.LOW]
|
||||
|
||||
|
||||
class TestSuccessPath:
|
||||
async def test_first_attempt_success_builds_response(self):
|
||||
mw, limiter, gate, transport, sleep, _ = _harness([_src("a")], [_ok("hello")])
|
||||
|
||||
Reference in New Issue
Block a user