feat: carry the reasoning verdict through to LLMResponse

Both assembly paths fill it, streaming and non-streaming alike. Filling
only one is exactly the divergence this issue exposed: M3 returns
reasoning prose over SSE and nothing at all over the plain endpoint, so
a verdict computed on one path says nothing about the other.

The field defaults to UNKNOWN on both TransportResult and LLMResponse.
A transport that does not judge should not get to declare absence on
the provider's behalf, and a default that stays silent is the only one
that cannot lie.
This commit is contained in:
2026-08-26 00:03:28 -04:00
parent 59d2e442e6
commit 8c5c23ae72
6 changed files with 157 additions and 3 deletions
+76 -1
View File
@@ -22,7 +22,7 @@ from polygateway.transports.openai_compat import (
_iter_sse_deltas,
_sse_data_payload,
)
from polygateway.types import ChatRequest, LLMResponse, SourceConfig
from polygateway.types import ChatRequest, LLMResponse, SourceConfig, ThinkingObservation
def _source(**overrides):
@@ -471,6 +471,81 @@ class TestReasoningTokens:
assert result.reasoning_tokens is None
class TestThinkingObservationVerdict:
"""issue #16/#17: 两条组装路径都必须裁定"推理到底发生没发生"
流式与非流式各测一遍是刻意的——只填一条路径正是本 issue 的根因形态:
库在其中一条路径上悄悄给出了不同的可观测性,下游无从分辨。
"""
def _reasoning_usage(self, reasoning):
return {**_USAGE, "completion_tokens_details": {"reasoning_tokens": reasoning}}
async def test_stream_reasoning_content_is_observed(self):
def handler(request):
return _sse_stream(
_chunk(reasoning="想一下"), _chunk(content="ok"), _chunk(usage=_USAGE)
)
result = await _complete(_transport_for(handler), _source())
assert result.thinking_observation is ThinkingObservation.OBSERVED
async def test_stream_without_any_signal_is_unknown(self):
"""无正文、无 details: 库不知道,就如实说不知道。"""
def handler(request):
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
result = await _complete(_transport_for(handler), _source())
assert result.thinking_observation is ThinkingObservation.UNKNOWN
async def test_stream_zero_reasoning_tokens_is_absent(self):
"""上游明确上报 0 才算 ABSENT——这是唯一的"确实没推理"证据。"""
def handler(request):
return _sse_stream(_chunk(content="ok"), _chunk(usage=self._reasoning_usage(0)))
result = await _complete(_transport_for(handler), _source())
assert result.thinking_observation is ThinkingObservation.ABSENT
async def test_non_stream_reasoning_content_is_observed(self):
def handler(request):
return httpx.Response(
200,
json={
"choices": [{"message": {"content": "42", "reasoning_content": "想一下"}}],
"usage": _USAGE,
},
)
result = await _complete(_transport_for(handler), _source(), stream=False)
assert result.thinking_observation is ThinkingObservation.OBSERVED
async def test_non_stream_without_any_signal_is_unknown(self):
"""M3 非流式实测形态: 推理已计费却既不回传正文也不回传 details。"""
def handler(request):
return httpx.Response(
200, json={"choices": [{"message": {"content": "42"}}], "usage": _USAGE}
)
result = await _complete(_transport_for(handler), _source(), stream=False)
assert result.thinking_observation is ThinkingObservation.UNKNOWN
async def test_non_stream_zero_reasoning_tokens_is_absent(self):
def handler(request):
return httpx.Response(
200,
json={
"choices": [{"message": {"content": "42"}}],
"usage": self._reasoning_usage(0),
},
)
result = await _complete(_transport_for(handler), _source(), stream=False)
assert result.thinking_observation is ThinkingObservation.ABSENT
class TestNonStreamFastPath:
async def test_non_stream_parses_message(self):
def handler(request):
+24
View File
@@ -27,6 +27,7 @@ from polygateway.types import (
GlobalLimits,
RetryPolicy,
SourceConfig,
ThinkingObservation,
TransportResult,
)
from tests.contracts.conftest import FakeClock
@@ -225,6 +226,29 @@ class TestObservabilityPassthrough:
assert resp.cached_prompt_tokens is None and resp.model_reported is None
assert resp.reasoning_tokens is None
async def test_thinking_observation_reaches_the_response(self):
"""issue #16/#17: 裁定归 transport,中间件只透传,不得在途中改判。"""
result = TransportResult(
content="ok",
thinking="想一下",
prompt_tokens=10,
completion_tokens=5,
usage_source="measured",
ttft_ms=12.0,
max_inter_token_ms=3.0,
raw={},
thinking_observation=ThinkingObservation.OBSERVED,
)
mw, *_ = _harness([_src("a")], [result])
resp = await mw(_REQ)
assert resp.thinking_observation is ThinkingObservation.OBSERVED
async def test_unjudged_transport_result_stays_unknown(self):
"""不裁定的 transport(如 OCR)透传出来仍是 UNKNOWN,不被默认成 ABSENT。"""
mw, *_ = _harness([_src("a")], [_ok()])
resp = await mw(_REQ)
assert resp.thinking_observation is ThinkingObservation.UNKNOWN
class TestRetryAndFailover:
async def test_transient_switches_source_then_succeeds(self):
+26
View File
@@ -88,6 +88,30 @@ class TestLLMResponse:
assert filled.model_reported == "MiniMax-Text-01-250321"
assert filled.reasoning_tokens == 0 # 上报了且确实没推理,不得与 None 混同
def test_thinking_observation_defaults_to_unknown(self):
"""issue #16/#17: 默认必须是 UNKNOWN——"没信号"不得被伪装成"没推理"
默认值取 ABSENT 会让每个不填该字段的构造点(测试 fake、其他 transport)
都在替上游做一个它没做过的声明,那正是本 issue 要消灭的静默错觉。
"""
resp = LLMResponse("c", "t", "m", "p", 1, 2, 3, None, None, False, "cid")
assert resp.thinking_observation is ThinkingObservation.UNKNOWN
filled = LLMResponse(
"c",
"t",
"m",
"p",
1,
2,
3,
None,
None,
False,
"cid",
thinking_observation=ThinkingObservation.OBSERVED,
)
assert filled.thinking_observation is ThinkingObservation.OBSERVED
def test_frozen(self):
resp = LLMResponse("c", "t", "m", "p", 1, 2, 3, None, None, False, "cid")
with pytest.raises(dataclasses.FrozenInstanceError):
@@ -264,6 +288,8 @@ class TestAuxTypes:
# issue #3: 新字段带默认值,不填也能构造(OCR 等其他 transport 零改动)
assert s.cached_prompt_tokens is None and s.model_reported is None
assert s.reasoning_tokens is None
# issue #16/#17: 不裁定的 transport 只能说"不知道",不能替上游说"没推理"
assert s.thinking_observation is ThinkingObservation.UNKNOWN
class TestOcrTypes: